agentic-kdd 2.0.7 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1345 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const http = require('http');
7
+ const { execSync } = require('child_process');
8
+
9
+ const PORT = 3847;
10
+ const projectPath = process.cwd();
11
+ const dbPath = path.join(projectPath, '.agentic', 'memoria.db');
12
+ const grafoPath = path.join(projectPath, '.agentic', 'grafo', 'grafo.js');
13
+ const configPath = path.join(projectPath, '.agentic', 'config.md');
14
+ const memoriaPath = path.join(projectPath, '.agentic', 'memoria');
15
+
16
+ if (!fs.existsSync(configPath)) { console.log('\n Agentic KDD not installed.\n'); process.exit(1); }
17
+ if (fs.existsSync(grafoPath)) { try { process.stdout.write(' Syncing... '); execSync(`node "${grafoPath}" sync`, { stdio: 'pipe', cwd: projectPath }); console.log('✓'); } catch {} }
18
+
19
+ function readConfig() {
20
+ try {
21
+ const c = fs.readFileSync(configPath, 'utf8');
22
+ const lines = c.split('\n');
23
+
24
+ const get = (key) => (c.match(new RegExp(key + ': (.+)')) || [])[1]?.trim() || '—';
25
+
26
+ // Busca una sección que EMPIECE con el prefijo dado y recoge hasta la siguiente sección del mismo nivel
27
+ const getBlock = (prefix, stopLevel) => {
28
+ const startIdx = lines.findIndex(l => l.trimStart().startsWith(prefix));
29
+ if (startIdx === -1) return '';
30
+ const stopRe = stopLevel === '##' ? /^## / : /^### /;
31
+ let result = [];
32
+ for (let i = startIdx + 1; i < lines.length; i++) {
33
+ if (lines[i].match(stopRe)) break;
34
+ result.push(lines[i]);
35
+ }
36
+ return result.join('\n').trim();
37
+ };
38
+
39
+ // Leer yaml block para stack
40
+ const getYaml = (key) => {
41
+ const m = c.match(new RegExp(' ' + key + ': (.+)'));
42
+ return m ? m[1].trim() : '—';
43
+ };
44
+
45
+ return {
46
+ nombre: get('Nombre'),
47
+ descripcion: (() => {
48
+ // Descripción puede ser multilinea con |
49
+ const m = c.match(/Descripción: \|\n([\s\S]*?)(?=\nTipo:|$)/);
50
+ if (m) return m[1].split('\n').map(l => l.trim()).filter(Boolean).join(' ');
51
+ return get('Descripción');
52
+ })(),
53
+ tipo: get('Tipo'),
54
+ framework: getYaml('framework'),
55
+ language: getYaml('language'),
56
+ runtime: getYaml('runtime'),
57
+ base_datos: getYaml('base_datos'),
58
+ package_manager: getYaml('package_manager'),
59
+ cmd_dev: getYaml('dev'),
60
+ cmd_test: getYaml('test'),
61
+ cmd_build: getYaml('build'),
62
+ implementados: getBlock('### Implementados', '###'),
63
+ pendientes: getBlock('### Pendientes', '##'),
64
+ reglas: getBlock('### Desarrollo', '###') || getBlock('## Reglas del proyecto', '##'),
65
+ raw: c
66
+ };
67
+ } catch { return {}; }
68
+ }
69
+
70
+ function readMemoria(file) { try { const p = path.join(memoriaPath, file); return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : ''; } catch { return ''; } }
71
+
72
+ function parseEntries(content) {
73
+ return content.split(/^## /m)
74
+ .filter(s => s.trim() && !s.startsWith('<!--') && !s.startsWith('Cómo') && !s.startsWith('Formato') && !s.startsWith('Registro') && !s.startsWith('Patrones') && s.length > 10)
75
+ .map(s => {
76
+ const lines = s.split('\n');
77
+ const titulo = lines[0].trim().replace(/^\[.*?\]\s*/, '').trim();
78
+ if (!titulo || titulo.length < 5) return null;
79
+ const get = (k) => { for (const l of lines) { if (l.startsWith(k + ':')) return l.split(':').slice(1).join(':').trim(); } return ''; };
80
+ return { titulo, area: get('Área') || get('Area') || 'global', confianza: get('Confianza') || 'BAJA', aplicado: parseInt(get('Aplicado')) || 0, util: parseInt(get('Útil') || get('Util')) || 0, estado: get('Estado') || 'ACTIVO', contenido: s };
81
+ }).filter(Boolean);
82
+ }
83
+
84
+ const config = readConfig();
85
+ const patrones = parseEntries(readMemoria('patrones.md')).filter(p => p.estado === 'ACTIVO');
86
+ const decisiones = parseEntries(readMemoria('decisiones.md'));
87
+ const errores = parseEntries(readMemoria('errores.md'));
88
+
89
+ function getGraphData() {
90
+ try {
91
+ if (!fs.existsSync(dbPath)) return { nodes: [], edges: [] };
92
+ const Database = require('better-sqlite3');
93
+ const db = new Database(dbPath, { readonly: true });
94
+ const nodes = db.prepare('SELECT * FROM nodos ORDER BY fecha_creacion DESC').all();
95
+ const edges = db.prepare('SELECT * FROM relaciones').all();
96
+ db.close();
97
+ return { nodes, edges };
98
+ } catch { return { nodes: [], edges: [] }; }
99
+ }
100
+
101
+ const { nodes, edges } = getGraphData();
102
+
103
+ // Calcular grado de conexiones por nodo (como Graphify — nodos divinos)
104
+ const degreeMap = {};
105
+ nodes.forEach(n => { degreeMap[n.id] = 0; });
106
+ edges.forEach(e => { degreeMap[e.desde_id] = (degreeMap[e.desde_id] || 0) + 1; degreeMap[e.hacia_id] = (degreeMap[e.hacia_id] || 0) + 1; });
107
+ const maxDegree = Math.max(...Object.values(degreeMap), 1);
108
+
109
+ // Nodos divinos = top 20% por conexiones
110
+ const godThreshold = maxDegree * 0.6;
111
+ const godNodes = nodes.filter(n => (degreeMap[n.id] || 0) >= godThreshold && godThreshold > 0);
112
+
113
+ // Conexiones sorprendentes = edges entre nodos de diferente área
114
+ const surprisingEdges = edges.filter(e => {
115
+ const src = nodes.find(n => n.id === e.desde_id);
116
+ const tgt = nodes.find(n => n.id === e.hacia_id);
117
+ return src && tgt && src.area !== tgt.area && src.area !== 'global' && tgt.area !== 'global';
118
+ });
119
+
120
+ const stats = {
121
+ total: nodes.length, errors: nodes.filter(n => n.tipo === 'error').length,
122
+ patterns: nodes.filter(n => n.tipo === 'patron').length, decisions: nodes.filter(n => n.tipo === 'decision').length,
123
+ high: nodes.filter(n => n.confianza === 'ALTA').length, medium: nodes.filter(n => n.confianza === 'MEDIA').length,
124
+ low: nodes.filter(n => n.confianza === 'BAJA').length, relations: edges.length,
125
+ active: nodes.filter(n => n.estado === 'ACTIVO').length, obsolete: nodes.filter(n => n.estado === 'OBSOLETO').length,
126
+ godNodes: godNodes.length, surprising: surprisingEdges.length,
127
+ };
128
+
129
+ function parseModulos(text) {
130
+ if (!text || text === '—') return [];
131
+ const lines = text.split('\n').map(l => l.trim()).filter(l => l);
132
+ const results = [];
133
+ for (const line of lines) {
134
+ // Tabla markdown: | 1 | Auth, middleware | /login |
135
+ if (line.startsWith('|') && !line.match(/^[|\s-]+$/) && !line.toLowerCase().includes('fase') && !line.toLowerCase().includes('módulo') && !line.toLowerCase().includes('module') && !line.toLowerCase().includes('tabla')) {
136
+ const cols = line.split('|').map(c => c.trim()).filter(Boolean);
137
+ if (cols.length >= 2) {
138
+ const modName = cols[1].replace(/\*\*/g, '').replace(/✅/g, '').trim();
139
+ if (modName && modName.length > 2) results.push(modName);
140
+ }
141
+ }
142
+ // Lista simple: - módulo o [ ] módulo
143
+ else if (line.match(/^[-*\[]/)) {
144
+ const clean = line.replace(/^[-*]\s*/, '').replace(/^\[.\]\s*/, '').trim();
145
+ if (clean && clean.length > 3 && !clean.startsWith('_')) results.push(clean);
146
+ }
147
+ }
148
+ return results;
149
+ }
150
+
151
+ const modulosImpl = parseModulos(config.implementados);
152
+ const modulosPend = parseModulos(config.pendientes);
153
+ const reglas = (config.reglas || '').split('\n').map(l => l.trim()).filter(l => l && l !== '—' && l.length > 5);
154
+
155
+ // Construir módulos con relaciones para el grafo neuronal de proyecto
156
+ function buildModuleGraph() {
157
+ const mNodes = [];
158
+ const mEdges = [];
159
+ const areaCount = {};
160
+
161
+ // Contar errores y patrones por área
162
+ nodes.forEach(n => {
163
+ if (!areaCount[n.area]) areaCount[n.area] = { errors: 0, patterns: 0, decisions: 0, high: 0 };
164
+ if (n.tipo === 'error') areaCount[n.area].errors++;
165
+ if (n.tipo === 'patron') areaCount[n.area].patterns++;
166
+ if (n.tipo === 'decision') areaCount[n.area].decisions++;
167
+ if (n.confianza === 'ALTA') areaCount[n.area].high++;
168
+ });
169
+
170
+ // Crear nodos de módulos implementados
171
+ modulosImpl.forEach((m, i) => {
172
+ const area = m.toLowerCase().replace(/\s+/g, '-').split(/[\s\/\-]/)[0];
173
+ const stats = areaCount[area] || areaCount['global'] || { errors: 0, patterns: 0, decisions: 0, high: 0 };
174
+ mNodes.push({ id: 'impl-' + i, label: m, tipo: 'impl', area, errors: stats.errors, patterns: stats.patterns, high: stats.high, degree: stats.errors + stats.patterns + stats.decisions });
175
+ });
176
+
177
+ // Crear nodos de módulos pendientes
178
+ modulosPend.forEach((m, i) => {
179
+ mNodes.push({ id: 'pend-' + i, label: m, tipo: 'pend', area: m.toLowerCase().split(/\s/)[0], errors: 0, patterns: 0, high: 0, degree: 0 });
180
+ });
181
+
182
+ // Crear edges entre módulos que comparten área en la memoria
183
+ const implByArea = {};
184
+ mNodes.filter(n => n.tipo === 'impl').forEach(n => {
185
+ if (!implByArea[n.area]) implByArea[n.area] = [];
186
+ implByArea[n.area].push(n.id);
187
+ });
188
+
189
+ // Conectar módulos con errores compartidos (misma área en memoria)
190
+ const areaRelations = {};
191
+ edges.forEach(e => {
192
+ const src = nodes.find(n => n.id === e.desde_id);
193
+ const tgt = nodes.find(n => n.id === e.hacia_id);
194
+ if (src && tgt) {
195
+ const key = [src.area, tgt.area].sort().join('::');
196
+ areaRelations[key] = (areaRelations[key] || 0) + 1;
197
+ }
198
+ });
199
+
200
+ // Conectar módulos impl con al menos 2 conexiones entre sus áreas
201
+ mNodes.filter(n => n.tipo === 'impl').forEach((src, si) => {
202
+ mNodes.filter((n, ti) => n.tipo === 'impl' && ti > si).forEach(tgt => {
203
+ const key = [src.area, tgt.area].sort().join('::');
204
+ if (areaRelations[key] >= 1) {
205
+ mEdges.push({ source: src.id, target: tgt.id, weight: areaRelations[key], tipo: 'shared_knowledge' });
206
+ }
207
+ });
208
+ });
209
+
210
+ // Siempre conectar módulos consecutivos como relación de flujo
211
+ mNodes.filter(n => n.tipo === 'impl').forEach((n, i, arr) => {
212
+ if (i < arr.length - 1) {
213
+ const exists = mEdges.find(e => (e.source === n.id && e.target === arr[i+1].id) || (e.source === arr[i+1].id && e.target === n.id));
214
+ if (!exists) mEdges.push({ source: n.id, target: arr[i+1].id, weight: 1, tipo: 'flow' });
215
+ }
216
+ });
217
+
218
+ // Conectar pendientes con el módulo impl más relacionado
219
+ mNodes.filter(n => n.tipo === 'pend').forEach(pend => {
220
+ if (mNodes.filter(n => n.tipo === 'impl').length > 0) {
221
+ const target = mNodes.filter(n => n.tipo === 'impl')[0];
222
+ mEdges.push({ source: pend.id, target: target.id, weight: 1, tipo: 'depends' });
223
+ }
224
+ });
225
+
226
+ return { mNodes, mEdges };
227
+ }
228
+
229
+ const { mNodes, mEdges } = buildModuleGraph();
230
+
231
+ // Preguntas sugeridas para el nuevo integrante (como GRAPH_REPORT de Graphify)
232
+ function buildSuggestedQuestions() {
233
+ const qs = [];
234
+ if (godNodes.length > 0) qs.push(`What flows through ${godNodes[0].titulo.slice(0,40)}?`);
235
+ if (surprisingEdges.length > 0) {
236
+ const e = surprisingEdges[0];
237
+ const src = nodes.find(n => n.id === e.desde_id);
238
+ const tgt = nodes.find(n => n.id === e.hacia_id);
239
+ if (src && tgt) qs.push(`How does ${src.area} connect to ${tgt.area}?`);
240
+ }
241
+ if (modulosImpl.length > 0) qs.push(`How do I add a feature to ${modulosImpl[0]}?`);
242
+ if (errores.length > 0) qs.push(`What errors should I avoid in ${errores[0].area}?`);
243
+ if (patrones.filter(p => p.confianza === 'ALTA').length > 0) qs.push(`What are the permanent rules for this project?`);
244
+ if (decisiones.length > 0) qs.push(`Why was ${decisiones[0].titulo.slice(0,40)} decided?`);
245
+ return qs.slice(0, 5);
246
+ }
247
+
248
+ const suggestedQuestions = buildSuggestedQuestions();
249
+
250
+ const HTML = `<!DOCTYPE html>
251
+ <html>
252
+ <head>
253
+ <meta charset="UTF-8">
254
+ <title>Agentic KDD — ${config.nombre}</title>
255
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
256
+ <style>
257
+ :root{--bg:#0a0d14;--bg2:#111520;--bg3:#1a1f2e;--bg4:#232840;--border:#2a3050;--text:#e2e8f0;--text2:#94a3b8;--text3:#64748b;--purple:#8b5cf6;--pl:#a78bfa;--green:#10b981;--red:#ef4444;--blue:#3b82f6;--amber:#f59e0b;--cyan:#06b6d4;--pink:#ec4899;--r:12px}
258
+ .light{--bg:#f0f4f8;--bg2:#ffffff;--bg3:#f8fafc;--bg4:#eef2f7;--border:#dde3ee;--text:#0f172a;--text2:#475569;--text3:#94a3b8}
259
+ *{box-sizing:border-box;margin:0;padding:0}
260
+ body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:14px;height:100vh;overflow:hidden;display:flex;flex-direction:column}
261
+
262
+ /* Header */
263
+ .hdr{background:var(--bg2);border-bottom:1px solid var(--border);padding:11px 20px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;gap:12px}
264
+ .logo{font-size:15px;font-weight:700;color:var(--pl);white-space:nowrap}
265
+ .proj{font-size:12px;color:var(--text2);background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:3px 8px;margin-left:10px;white-space:nowrap}
266
+ .dot{width:7px;height:7px;border-radius:50%;background:var(--green);margin-left:8px;display:inline-block;flex-shrink:0}
267
+ .hdr-r{display:flex;align-items:center;gap:6px;flex-shrink:0}
268
+ .badge{font-size:10px;padding:3px 7px;border-radius:4px;font-weight:600;white-space:nowrap}
269
+ .b-god{background:rgba(245,158,11,.2);color:#fbbf24;border:1px solid rgba(245,158,11,.3)}
270
+ .b-sur{background:rgba(236,72,153,.15);color:#f472b6;border:1px solid rgba(236,72,153,.25)}
271
+ .b-high{background:rgba(139,92,246,.2);color:#a78bfa;border:1px solid rgba(139,92,246,.3)}
272
+ .btn{background:var(--bg3);border:1px solid var(--border);color:var(--text2);border-radius:6px;padding:5px 10px;font-size:11px;cursor:pointer;transition:all .15s;white-space:nowrap}
273
+ .btn:hover{border-color:var(--purple);color:var(--pl)}
274
+ .sel{background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:5px 8px;font-size:11px}
275
+
276
+ /* Mode tabs */
277
+ .mode-tabs{background:var(--bg2);border-bottom:1px solid var(--border);padding:0 20px;display:flex;flex-shrink:0}
278
+ .mode-tab{padding:11px 18px;font-size:13px;font-weight:500;cursor:pointer;color:var(--text3);border-bottom:2px solid transparent;transition:all .15s;display:flex;align-items:center;gap:6px;white-space:nowrap}
279
+ .mode-tab:hover{color:var(--text2)}
280
+ .mode-tab.active{color:var(--pl);border-bottom-color:var(--purple)}
281
+
282
+ .content{flex:1;overflow:hidden;display:flex}
283
+
284
+ /* ════════ KNOWLEDGE GRAPH MODE ════════ */
285
+ #mode-graph{flex:1;display:flex;overflow:hidden}
286
+ .sidebar{width:272px;flex-shrink:0;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
287
+ .sb-tabs{display:flex;border-bottom:1px solid var(--border)}
288
+ .sb-tab{flex:1;padding:9px 6px;text-align:center;font-size:11px;cursor:pointer;color:var(--text3);border-bottom:2px solid transparent;transition:all .15s}
289
+ .sb-tab.active{color:var(--pl);border-bottom-color:var(--purple)}
290
+ .sb-body{flex:1;overflow-y:auto;padding:10px}
291
+ .sb-body::-webkit-scrollbar{width:3px}
292
+ .sb-body::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
293
+ .search-box{padding:8px 10px;border-bottom:1px solid var(--border)}
294
+ .search-input{width:100%;background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:5px 8px;font-size:11px;outline:none}
295
+ .search-input:focus{border-color:var(--purple)}
296
+ .filter-row{padding:6px 10px;border-bottom:1px solid var(--border);display:flex;gap:4px;flex-wrap:wrap}
297
+ .fpill{font-size:10px;padding:2px 7px;border-radius:10px;border:1px solid var(--border);background:var(--bg3);color:var(--text2);cursor:pointer;transition:all .15s}
298
+ .fpill.active{background:rgba(139,92,246,.15);border-color:var(--purple);color:var(--pl)}
299
+
300
+ /* God nodes section */
301
+ .god-section{padding:8px 10px;border-bottom:1px solid var(--border);background:rgba(245,158,11,.04)}
302
+ .god-title{font-size:9px;color:var(--amber);text-transform:uppercase;letter-spacing:.08em;font-weight:700;margin-bottom:5px;display:flex;align-items:center;gap:4px}
303
+ .god-item{font-size:10px;color:var(--text2);padding:3px 0;display:flex;align-items:center;gap:5px;cursor:pointer}
304
+ .god-item:hover{color:var(--amber)}
305
+ .god-ring{width:10px;height:10px;border-radius:50%;border:2px solid var(--amber);flex-shrink:0}
306
+
307
+ .nitem{padding:7px 8px;border-radius:6px;cursor:pointer;margin-bottom:3px;border:1px solid transparent;transition:all .15s}
308
+ .nitem:hover{background:var(--bg3);border-color:var(--border)}
309
+ .nitem.selected{background:rgba(139,92,246,.1);border-color:var(--purple)}
310
+ .nitem.god-node{border-left:2px solid var(--amber)}
311
+ .ntb{font-size:9px;font-weight:700;padding:2px 5px;border-radius:3px;flex-shrink:0}
312
+ .t-error{background:rgba(239,68,68,.15);color:#f87171}
313
+ .t-patron{background:rgba(16,185,129,.15);color:#34d399}
314
+ .t-decision{background:rgba(59,130,246,.15);color:#60a5fa}
315
+ .mb{font-size:9px;padding:1px 4px;border-radius:3px;font-weight:500}
316
+ .cALTA{background:rgba(16,185,129,.2);color:#34d399}
317
+ .cMEDIA{background:rgba(245,158,11,.2);color:#fbbf24}
318
+ .cBAJA{background:rgba(100,116,139,.2);color:#94a3b8}
319
+ .ab{font-size:9px;color:var(--text3);background:var(--bg3);border-radius:3px;padding:1px 4px}
320
+ .tag-ext{font-size:9px;padding:1px 4px;border-radius:3px;background:rgba(16,185,129,.1);color:#6ee7b7;border:1px solid rgba(16,185,129,.2)}
321
+ .tag-inf{font-size:9px;padding:1px 4px;border-radius:3px;background:rgba(59,130,246,.1);color:#93c5fd;border:1px solid rgba(59,130,246,.2)}
322
+ .tag-amb{font-size:9px;padding:1px 4px;border-radius:3px;background:rgba(245,158,11,.1);color:#fcd34d;border:1px solid rgba(245,158,11,.2)}
323
+
324
+ /* Stats panel */
325
+ .mini-stats{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:10px}
326
+ .ms{background:var(--bg3);border-radius:7px;padding:8px;text-align:center}
327
+ .ms-v{font-size:22px;font-weight:700;line-height:1}
328
+ .ms-l{font-size:9px;color:var(--text3);margin-top:2px}
329
+ .conf-row{display:flex;align-items:center;gap:7px;margin-bottom:7px}
330
+ .conf-label{font-size:10px;width:56px;flex-shrink:0}
331
+ .conf-bw{flex:1;background:var(--bg3);border-radius:3px;height:4px;overflow:hidden}
332
+ .conf-bar{height:100%;border-radius:3px;transition:width .6s}
333
+ .conf-n{font-size:10px;color:var(--text3);width:16px;text-align:right}
334
+
335
+ /* Surprising connections */
336
+ .sur-section{padding:8px 10px;border-bottom:1px solid var(--border);background:rgba(236,72,153,.03)}
337
+ .sur-title{font-size:9px;color:var(--pink);text-transform:uppercase;letter-spacing:.08em;font-weight:700;margin-bottom:5px}
338
+ .sur-item{font-size:10px;color:var(--text2);padding:3px 0;cursor:pointer;display:flex;gap:4px;align-items:flex-start;line-height:1.4}
339
+ .sur-item:hover{color:var(--pink)}
340
+ .sur-dot{color:var(--pink);flex-shrink:0}
341
+
342
+ /* Graph area */
343
+ .graph-area{flex:1;position:relative;overflow:hidden;background:var(--bg)}
344
+ #gc{width:100%;height:100%}
345
+ .gtt{position:absolute;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:11px;pointer-events:none;opacity:0;transition:opacity .15s;z-index:15;max-width:240px;box-shadow:0 4px 20px rgba(0,0,0,.5)}
346
+ .graph-legend{position:absolute;top:10px;left:10px;background:rgba(17,21,32,.9);border:1px solid var(--border);border-radius:8px;padding:8px 12px;display:flex;gap:10px;backdrop-filter:blur(4px)}
347
+ .lg-item{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--text2)}
348
+ .lg-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
349
+ .graph-controls{position:absolute;bottom:12px;left:12px;display:flex;gap:6px}
350
+ .gc-btn{background:rgba(17,21,32,.9);border:1px solid var(--border);color:var(--text2);border-radius:6px;padding:5px 9px;font-size:10px;cursor:pointer;backdrop-filter:blur(4px)}
351
+ .gc-btn:hover{border-color:var(--purple);color:var(--pl)}
352
+
353
+ /* Detail panel */
354
+ .detail-panel{position:absolute;right:12px;top:12px;width:272px;background:var(--bg2);border:1px solid var(--border);border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.6);z-index:20;display:none;max-height:calc(100% - 24px);overflow-y:auto;backdrop-filter:blur(8px)}
355
+ .detail-panel.visible{display:block}
356
+ .dp-header{padding:12px 14px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
357
+ .dp-title{font-size:12px;font-weight:600;color:var(--text);line-height:1.4;flex:1}
358
+ .dp-close{cursor:pointer;color:var(--text3);font-size:16px;flex-shrink:0;line-height:1}
359
+ .dp-close:hover{color:var(--text)}
360
+ .dp-body{padding:12px 14px}
361
+ .dp-section{margin-bottom:12px}
362
+ .dp-label{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:5px;font-weight:600}
363
+ .dp-val{font-size:11px;color:var(--text2);line-height:1.6}
364
+ .dp-badges{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:10px}
365
+ .rel-item{display:flex;align-items:center;gap:6px;padding:5px 7px;background:var(--bg3);border-radius:5px;margin-bottom:3px;cursor:pointer;transition:all .15s}
366
+ .rel-item:hover{background:rgba(139,92,246,.1);border:1px solid rgba(139,92,246,.3)}
367
+ .rel-name{font-size:11px;color:var(--text);flex:1}
368
+ .rel-type-label{font-size:9px;color:var(--text3);background:var(--bg4);border-radius:3px;padding:1px 4px}
369
+ .conf-progress{height:3px;background:var(--bg3);border-radius:2px;overflow:hidden;margin-top:5px}
370
+ .conf-progress-fill{height:100%;border-radius:2px;transition:width .5s}
371
+
372
+ /* ════════ PROJECT DOCS MODE ════════ */
373
+ #mode-docs{flex:1;display:none;overflow:hidden}
374
+ .docs-layout{display:flex;height:100%;overflow:hidden}
375
+ .docs-nav{width:210px;flex-shrink:0;background:var(--bg2);border-right:1px solid var(--border);overflow-y:auto;padding:12px}
376
+ .docs-nav::-webkit-scrollbar{width:3px}
377
+ .docs-nav::-webkit-scrollbar-thumb{background:var(--border)}
378
+ .nav-section{margin-bottom:14px}
379
+ .nav-title{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;font-weight:700;margin-bottom:5px;padding:0 4px}
380
+ .nav-item{font-size:12px;color:var(--text2);padding:6px 8px;border-radius:6px;cursor:pointer;margin-bottom:2px;transition:all .15s;display:flex;align-items:center;gap:6px}
381
+ .nav-item:hover{background:var(--bg3);color:var(--text)}
382
+ .nav-item.active{background:rgba(139,92,246,.12);color:var(--pl);border-left:2px solid var(--purple);padding-left:6px}
383
+ .nav-count{font-size:10px;color:var(--text3);margin-left:auto;background:var(--bg3);border-radius:10px;padding:1px 5px}
384
+ .docs-main{flex:1;overflow-y:auto;padding:24px 28px}
385
+ .docs-main::-webkit-scrollbar{width:4px}
386
+ .docs-main::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
387
+ .docs-section{display:none}.docs-section.active{display:block}#doc-modules.active{display:flex}
388
+ .docs-section.active{display:block}
389
+ .docs-h1{font-size:20px;font-weight:700;color:var(--text);margin-bottom:6px}
390
+ .docs-h2{font-size:14px;font-weight:600;color:var(--text);margin:20px 0 10px;display:flex;align-items:center;gap:8px}
391
+ .docs-sub{font-size:13px;color:var(--text2);margin-bottom:20px;line-height:1.7}
392
+
393
+ /* Module graph container */
394
+ #mod-graph{width:100%;height:500px;display:block;background:var(--bg)}
395
+
396
+ /* Info cards */
397
+ .info-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:16px}
398
+ .info-card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px}
399
+ .ic-label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px}
400
+ .ic-val{font-size:14px;font-weight:700;color:var(--text)}
401
+ .stack-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
402
+ .stack-item{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:12px}
403
+ .si-label{font-size:10px;color:var(--text3);margin-bottom:4px}
404
+ .si-val{font-size:13px;color:var(--text);font-weight:500}
405
+
406
+ /* Module cards */
407
+ .module-card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:12px 14px;margin-bottom:6px;display:flex;align-items:center;gap:10px;transition:all .15s;cursor:default}
408
+ .module-card.impl{border-left:3px solid var(--green)}
409
+ .module-card.impl:hover{border-color:var(--green);background:rgba(16,185,129,.04)}
410
+ .module-card.pend{border-left:3px solid var(--amber);opacity:.7}
411
+ .mod-name{font-size:13px;color:var(--text);font-weight:500;flex:1}
412
+ .mod-status{font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600}
413
+ .ms-impl{background:rgba(16,185,129,.15);color:#34d399}
414
+ .ms-pend{background:rgba(245,158,11,.15);color:#fbbf24}
415
+ .mod-stats{display:flex;gap:5px}
416
+ .mod-stat{font-size:10px;padding:1px 5px;border-radius:3px;background:var(--bg3);color:var(--text3)}
417
+
418
+ /* Patterns */
419
+ .pattern-card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:8px;transition:all .15s}
420
+ .pattern-card:hover{border-color:var(--border);}
421
+ .pattern-card.high{border-left:3px solid var(--green)}
422
+ .pc-top{display:flex;align-items:center;gap:8px;margin-bottom:8px}
423
+ .pc-title{font-size:13px;font-weight:500;color:var(--text);flex:1}
424
+ .usage-bar{height:2px;background:var(--bg3);border-radius:1px;overflow:hidden;margin-top:6px}
425
+ .usage-fill{height:100%;border-radius:1px;background:var(--purple);transition:width .5s}
426
+
427
+ /* Decisions */
428
+ .decision-card{background:var(--bg2);border:1px solid var(--border);border-left:3px solid var(--blue);border-radius:8px;padding:12px 14px;margin-bottom:8px}
429
+ .dc-title{font-size:13px;font-weight:500;color:var(--text);margin-bottom:4px}
430
+ .dc-body{font-size:11px;color:var(--text2);line-height:1.6}
431
+
432
+ /* Rules */
433
+ .rule-item{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-bottom:6px;display:flex;align-items:flex-start;gap:8px;font-size:12px;color:var(--text2);line-height:1.5}
434
+ .rule-dot{width:5px;height:5px;border-radius:50%;background:var(--purple);flex-shrink:0;margin-top:5px}
435
+
436
+ /* Suggested questions */
437
+ .question-card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:10px 14px;margin-bottom:6px;font-size:12px;color:var(--text2);cursor:pointer;display:flex;align-items:center;gap:8px;transition:all .15s}
438
+ .question-card:hover{border-color:var(--purple);color:var(--pl);background:rgba(139,92,246,.04)}
439
+ .question-arrow{color:var(--text3);font-size:14px;margin-left:auto}
440
+
441
+ /* GRAPH REPORT section */
442
+ .report-section{background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(6,182,212,.05));border:1px solid rgba(139,92,246,.2);border-radius:12px;padding:16px;margin-bottom:20px}
443
+ .report-title{font-size:12px;font-weight:700;color:var(--pl);margin-bottom:10px;display:flex;align-items:center;gap:6px}
444
+ .report-item{font-size:11px;color:var(--text2);padding:4px 0;display:flex;gap:6px;border-bottom:1px solid rgba(255,255,255,.04);line-height:1.5}
445
+ .report-item:last-child{border-bottom:none}
446
+ .report-key{color:var(--text3);flex-shrink:0;min-width:80px}
447
+
448
+ /* Commands */
449
+ .cmd-row{display:flex;align-items:center;gap:10px;padding:7px 10px;background:var(--bg3);border-radius:6px;margin-bottom:4px;font-family:monospace}
450
+ .cmd-label{font-size:10px;color:var(--text3);width:58px;flex-shrink:0}
451
+ .cmd-val{font-size:11px;color:var(--cyan)}
452
+
453
+ /* Actions */
454
+ .docs-actions{display:flex;gap:8px;margin-bottom:16px}
455
+ .action-btn{background:var(--bg2);border:1px solid var(--border);color:var(--text2);border-radius:8px;padding:8px 14px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;transition:all .15s}
456
+ .action-btn:hover{border-color:var(--purple);color:var(--pl)}
457
+
458
+ .empty-state{text-align:center;padding:30px;color:var(--text3);font-size:12px}
459
+ .divider{border:none;border-top:1px solid var(--border);margin:16px 0}
460
+
461
+ @media print{.hdr,.mode-tabs,.docs-nav,.docs-actions{display:none!important}.docs-main{padding:0}}
462
+ </style>
463
+ </head>
464
+ <body id="app">
465
+
466
+ <header class="hdr">
467
+ <div style="display:flex;align-items:center;min-width:0">
468
+ <div class="logo">🤖 Agentic KDD</div>
469
+ <div class="proj">${config.nombre}</div>
470
+ <div class="dot"></div>
471
+ </div>
472
+ <div class="hdr-r">
473
+ ${godNodes.length > 0 ? `<span class="badge b-god">⚡ ${stats.godNodes} divine</span>` : ''}
474
+ ${surprisingEdges.length > 0 ? `<span class="badge b-sur">✨ ${stats.surprising} surprising</span>` : ''}
475
+ <span class="badge b-high">★ ${stats.high} HIGH</span>
476
+ <select class="sel" onchange="setLang(this.value)">
477
+ <option value="en">🇺🇸 EN</option>
478
+ <option value="es">🇪🇸 ES</option>
479
+ </select>
480
+ <button class="btn" onclick="toggleTheme()" id="tbtn">🌙 Dark</button>
481
+ </div>
482
+ </header>
483
+
484
+ <div class="mode-tabs">
485
+ <div class="mode-tab active" onclick="setMode('graph',this)">🧠 <span data-i="tab_graph">Knowledge Graph</span></div>
486
+ <div class="mode-tab" onclick="setMode('docs',this)">📚 <span data-i="tab_docs">Project Docs</span></div>
487
+ </div>
488
+
489
+ <div class="content">
490
+
491
+ <!-- ════════ KNOWLEDGE GRAPH ════════ -->
492
+ <div id="mode-graph">
493
+ <div class="sidebar">
494
+ <div class="sb-tabs">
495
+ <div class="sb-tab active" onclick="showSbTab('nodes',this)" data-i="sb_nodes">Nodes</div>
496
+ <div class="sb-tab" onclick="showSbTab('report',this)" data-i="sb_report">Report</div>
497
+ <div class="sb-tab" onclick="showSbTab('stats',this)" data-i="sb_stats">Stats</div>
498
+ </div>
499
+
500
+ <!-- NODES TAB -->
501
+ <div id="sbt-nodes" style="display:flex;flex-direction:column;flex:1;overflow:hidden">
502
+ ${godNodes.length > 0 ? `
503
+ <div class="god-section">
504
+ <div class="god-title">⚡ <span data-i="divine_nodes">Divine nodes</span></div>
505
+ ${godNodes.slice(0,3).map(n => `<div class="god-item" onclick="selectNode(${n.id})"><div class="god-ring"></div><span>${n.titulo.slice(0,36)}${n.titulo.length>36?'…':''}</span></div>`).join('')}
506
+ </div>` : ''}
507
+ ${surprisingEdges.length > 0 ? `
508
+ <div class="sur-section">
509
+ <div class="sur-title">✨ <span data-i="surprising">Surprising connections</span></div>
510
+ ${surprisingEdges.slice(0,2).map(e => {
511
+ const src = nodes.find(n => n.id === e.desde_id);
512
+ const tgt = nodes.find(n => n.id === e.hacia_id);
513
+ return src && tgt ? `<div class="sur-item" onclick="highlightEdge(${e.desde_id},${e.hacia_id})"><span class="sur-dot">⟶</span><span>${src.area} connects to ${tgt.area}</span></div>` : '';
514
+ }).join('')}
515
+ </div>` : ''}
516
+ <div class="search-box"><input class="search-input" placeholder="Search nodes..." id="srch" oninput="filterSearch(this.value)"></div>
517
+ <div class="filter-row">
518
+ <div class="fpill active" onclick="setFilter('all',this)" data-i="f_all">All</div>
519
+ <div class="fpill" onclick="setFilter('error',this)" data-i="f_err">Errors</div>
520
+ <div class="fpill" onclick="setFilter('patron',this)" data-i="f_pat">Patterns</div>
521
+ <div class="fpill" onclick="setFilter('decision',this)" data-i="f_dec">Decisions</div>
522
+ <div class="fpill" onclick="setFilter('ALTA',this)" data-i="f_high">★ HIGH</div>
523
+ <div class="fpill" onclick="setFilter('god',this)" data-i="f_god">⚡ Divine</div>
524
+ </div>
525
+ <div class="sb-body" id="nodes-list"></div>
526
+ </div>
527
+
528
+ <!-- REPORT TAB (like Graphify GRAPH_REPORT) -->
529
+ <div id="sbt-report" style="display:none" class="sb-body">
530
+ <div style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;font-weight:700;margin-bottom:10px" data-i="graph_report">Graph Report</div>
531
+ ${godNodes.length > 0 ? `
532
+ <div style="margin-bottom:14px">
533
+ <div style="font-size:10px;color:var(--amber);font-weight:600;margin-bottom:6px">⚡ Divine Nodes</div>
534
+ <div style="font-size:11px;color:var(--text3);margin-bottom:6px;line-height:1.5">Most connected — everything flows through them</div>
535
+ ${godNodes.map(n => `<div style="padding:5px 7px;background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.15);border-radius:5px;margin-bottom:3px;cursor:pointer" onclick="selectNode(${n.id})"><div style="font-size:11px;color:var(--amber)">${n.titulo.slice(0,44)}${n.titulo.length>44?'…':''}</div><div style="font-size:10px;color:var(--text3)">${degreeMap[n.id]||0} connections · ${n.area}</div></div>`).join('')}
536
+ </div>` : ''}
537
+ ${surprisingEdges.length > 0 ? `
538
+ <div style="margin-bottom:14px">
539
+ <div style="font-size:10px;color:var(--pink);font-weight:600;margin-bottom:6px">✨ Surprising Connections</div>
540
+ <div style="font-size:11px;color:var(--text3);margin-bottom:6px;line-height:1.5">Links between nodes from different areas</div>
541
+ ${surprisingEdges.slice(0,4).map(e => { const src = nodes.find(n=>n.id===e.desde_id); const tgt = nodes.find(n=>n.id===e.hacia_id); return src&&tgt?`<div style="padding:5px 7px;background:rgba(236,72,153,.05);border:1px solid rgba(236,72,153,.15);border-radius:5px;margin-bottom:3px;cursor:pointer;font-size:10px;color:var(--text2);line-height:1.5" onclick="highlightEdge(${e.desde_id},${e.hacia_id})">${src.area} <span style="color:var(--pink)">→</span> ${tgt.area}</div>`:''; }).join('')}
542
+ </div>` : ''}
543
+ <div>
544
+ <div style="font-size:10px;color:var(--pl);font-weight:600;margin-bottom:6px">💡 Suggested Questions</div>
545
+ ${suggestedQuestions.map(q => `<div style="padding:5px 7px;background:rgba(139,92,246,.06);border:1px solid rgba(139,92,246,.15);border-radius:5px;margin-bottom:3px;font-size:10px;color:var(--text2);line-height:1.4">${q}</div>`).join('')}
546
+ </div>
547
+ </div>
548
+
549
+ <!-- STATS TAB -->
550
+ <div id="sbt-stats" style="display:none" class="sb-body">
551
+ <div class="mini-stats">
552
+ <div class="ms"><div class="ms-v" style="color:var(--pl)">${stats.total}</div><div class="ms-l" data-i="s_total">nodes</div></div>
553
+ <div class="ms"><div class="ms-v" style="color:var(--cyan)">${stats.relations}</div><div class="ms-l" data-i="s_rel">relations</div></div>
554
+ <div class="ms"><div class="ms-v" style="color:var(--amber)">${stats.godNodes}</div><div class="ms-l" data-i="s_god">divine</div></div>
555
+ <div class="ms"><div class="ms-v" style="color:var(--green)">${stats.high}</div><div class="ms-l" data-i="s_high">HIGH</div></div>
556
+ </div>
557
+ <div style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px">Confidence</div>
558
+ <div class="conf-row"><div class="conf-label" style="color:#34d399">HIGH</div><div class="conf-bw"><div class="conf-bar" style="width:${stats.total?Math.round(stats.high/stats.total*100):0}%;background:#10b981"></div></div><div class="conf-n">${stats.high}</div></div>
559
+ <div class="conf-row"><div class="conf-label" style="color:#fbbf24">MED</div><div class="conf-bw"><div class="conf-bar" style="width:${stats.total?Math.round(stats.medium/stats.total*100):0}%;background:#f59e0b"></div></div><div class="conf-n">${stats.medium}</div></div>
560
+ <div class="conf-row"><div class="conf-label" style="color:#94a3b8">LOW</div><div class="conf-bw"><div class="conf-bar" style="width:${stats.total?Math.round(stats.low/stats.total*100):0}%;background:#475569"></div></div><div class="conf-n">${stats.low}</div></div>
561
+ <div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--border);font-size:11px;color:var(--text2);line-height:2.2">
562
+ <span style="color:#34d399">→ HIGH</span>: 7+ uses · 80%+ useful<br>
563
+ <span style="color:#fbbf24">→ MED</span>: 3+ uses · 70%+ useful<br>
564
+ <span style="color:var(--pink)">★ Divine</span>: ${Math.round(godThreshold)}+ connections
565
+ </div>
566
+ <div style="margin-top:12px;padding-top:10px;border-top:1px solid var(--border)">
567
+ <div style="font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px">By type</div>
568
+ <div style="font-size:12px;color:var(--text2);line-height:2.2">
569
+ <span style="color:#f87171">errors:</span> ${stats.errors} &nbsp; <span style="color:#34d399">patterns:</span> ${stats.patterns} &nbsp; <span style="color:#60a5fa">decisions:</span> ${stats.decisions}
570
+ </div>
571
+ </div>
572
+ </div>
573
+ </div>
574
+
575
+ <!-- GRAPH -->
576
+ <div class="graph-area">
577
+ <svg id="gc"></svg>
578
+ <div class="gtt" id="gtt"></div>
579
+ <div class="graph-legend">
580
+ <div class="lg-item"><div class="lg-dot" style="background:#ef4444"></div><span data-i="l_err">error</span></div>
581
+ <div class="lg-item"><div class="lg-dot" style="background:#10b981"></div><span data-i="l_pat">pattern</span></div>
582
+ <div class="lg-item"><div class="lg-dot" style="background:#3b82f6"></div><span data-i="l_dec">decision</span></div>
583
+ <div class="lg-item"><div class="lg-dot" style="background:transparent;border:2px solid #f59e0b;box-sizing:border-box"></div><span style="color:var(--amber)" data-i="l_divine">divine</span></div>
584
+ </div>
585
+ <div class="graph-controls">
586
+ <button class="gc-btn" onclick="resetGraph()" data-i="btn_reset">⟳ Reset</button>
587
+ <button class="gc-btn" onclick="centerGraph()" data-i="btn_center">⊙ Center</button>
588
+ <button class="gc-btn" onclick="toggleLabels()" id="label-btn" data-i="btn_labels">Labels OFF</button>
589
+ </div>
590
+ <div class="detail-panel" id="detail-panel">
591
+ <div class="dp-header">
592
+ <div class="dp-title" id="dp-title"></div>
593
+ <div class="dp-close" onclick="closeDetail()">×</div>
594
+ </div>
595
+ <div class="dp-body" id="dp-body"></div>
596
+ </div>
597
+ ${stats.total === 0 ? '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:var(--text3)"><div style="font-size:40px;margin-bottom:10px">🧠</div><div>No nodes yet — use aa: to start</div></div>' : ''}
598
+ </div>
599
+ </div>
600
+
601
+ <!-- ════════ PROJECT DOCS ════════ -->
602
+ <div id="mode-docs">
603
+ <div class="docs-layout">
604
+ <nav class="docs-nav">
605
+ <div class="nav-section">
606
+ <div class="nav-title" data-i="nav_overview">Overview</div>
607
+ <div class="nav-item active" onclick="showDoc('overview',this)">🏠 <span data-i="nav_project">Project</span></div>
608
+ <div class="nav-item" onclick="showDoc('stack',this)">⚙️ <span data-i="nav_stack">Stack</span></div>
609
+ <div class="nav-item" onclick="showDoc('commands',this)">💻 <span data-i="nav_commands">Commands</span></div>
610
+ </div>
611
+ <div class="nav-section">
612
+ <div class="nav-title" data-i="nav_arch">Architecture</div>
613
+ <div class="nav-item" onclick="showDoc('modules',this)">📦 <span data-i="nav_modules">Modules</span> <span class="nav-count">${modulosImpl.length + modulosPend.length}</span></div>
614
+ <div class="nav-item" onclick="showDoc('rules',this)">📋 <span data-i="nav_rules">Rules</span> <span class="nav-count">${reglas.length}</span></div>
615
+ </div>
616
+ <div class="nav-section">
617
+ <div class="nav-title" data-i="nav_knowledge">Knowledge</div>
618
+ <div class="nav-item" onclick="showDoc('patterns',this)">🟢 <span data-i="nav_patterns">Patterns</span> <span class="nav-count">${patrones.length}</span></div>
619
+ <div class="nav-item" onclick="showDoc('decisions',this)">🔵 <span data-i="nav_decisions">Decisions</span> <span class="nav-count">${decisiones.length}</span></div>
620
+ <div class="nav-item" onclick="showDoc('errors',this)">🔴 <span data-i="nav_errors">Errors</span> <span class="nav-count">${errores.length}</span></div>
621
+ <div class="nav-item" onclick="showDoc('questions',this)">💡 <span data-i="nav_questions">For New Devs</span></div>
622
+ </div>
623
+ </nav>
624
+
625
+ <div class="docs-main">
626
+
627
+ <!-- OVERVIEW -->
628
+ <div class="docs-section active" id="doc-overview">
629
+ <div class="docs-h1">${config.nombre}</div>
630
+ <div class="docs-sub">${config.descripcion !== '—' ? config.descripcion : 'No description yet — run aa: configurar'}</div>
631
+ <div class="docs-actions">
632
+ <button class="action-btn" onclick="window.print()">🖨️ <span data-i="btn_print">Print / Export PDF</span></button>
633
+ <button class="action-btn" onclick="copyMarkdown()">📋 <span data-i="btn_copy">Copy as Markdown</span></button>
634
+ </div>
635
+ <div class="info-grid">
636
+ <div class="info-card"><div class="ic-label">Type</div><div class="ic-val">${config.tipo || '—'}</div></div>
637
+ <div class="info-card"><div class="ic-label">Modules</div><div class="ic-val">${modulosImpl.length} <span style="color:var(--text3);font-size:12px">impl</span> · ${modulosPend.length} <span style="color:var(--text3);font-size:12px">pending</span></div></div>
638
+ <div class="info-card"><div class="ic-label">Knowledge</div><div class="ic-val">${stats.total} <span style="color:var(--text3);font-size:12px">nodes</span> · ${stats.high} <span style="color:var(--text3);font-size:12px">HIGH</span></div></div>
639
+ </div>
640
+ <div class="report-section">
641
+ <div class="report-title">📊 Graph Report <span style="font-size:10px;color:var(--text3);font-weight:400">— like Graphify's GRAPH_REPORT.md</span></div>
642
+ ${godNodes.length > 0 ? `<div class="report-item"><span class="report-key">Divine nodes</span><span>${godNodes.map(n=>n.titulo.slice(0,30)).join(', ')}</span></div>` : ''}
643
+ ${surprisingEdges.length > 0 ? `<div class="report-item"><span class="report-key">Surprising</span><span>${surprisingEdges.length} cross-area connections found</span></div>` : ''}
644
+ <div class="report-item"><span class="report-key">HIGH rules</span><span>${patrones.filter(p=>p.confianza==='ALTA').map(p=>p.titulo.slice(0,25)).join(' · ') || 'None yet'}</span></div>
645
+ <div class="report-item"><span class="report-key">Most errors</span><span>${errores.length > 0 ? errores.sort((a,b)=>b.aplicado-a.aplicado)[0].titulo.slice(0,40) : 'None yet'}</span></div>
646
+ </div>
647
+ <div class="docs-h2">🚀 Getting started</div>
648
+ <div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px;font-size:12px;color:var(--text2);line-height:2.2">
649
+ <strong style="color:var(--text)">1.</strong> Open in Cursor or Claude Code<br>
650
+ <strong style="color:var(--text)">2.</strong> Type <code style="color:var(--pl);background:var(--bg3);padding:1px 6px;border-radius:3px">aa: help</code> to see all commands<br>
651
+ <strong style="color:var(--text)">3.</strong> Type <code style="color:var(--cyan);background:var(--bg3);padding:1px 6px;border-radius:3px">aa: [your task]</code> to start developing<br>
652
+ <strong style="color:var(--text)">4.</strong> Type <code style="color:var(--amber);background:var(--bg3);padding:1px 6px;border-radius:3px">audit: auditar</code> before going to production
653
+ </div>
654
+ </div>
655
+
656
+ <!-- STACK -->
657
+ <div class="docs-section" id="doc-stack">
658
+ <div class="docs-h1" data-i="h_stack">Tech Stack</div>
659
+ <div class="docs-sub" data-i="sub_stack">Technologies and frameworks used in this project.</div>
660
+ <div class="stack-grid">
661
+ <div class="stack-item"><div class="si-label">Framework</div><div class="si-val">${config.framework || '—'}</div></div>
662
+ <div class="stack-item"><div class="si-label">Language</div><div class="si-val">${config.language || '—'}</div></div>
663
+ <div class="stack-item"><div class="si-label">Runtime</div><div class="si-val">${config.runtime || '—'}</div></div>
664
+ <div class="stack-item"><div class="si-label">Database</div><div class="si-val">${config.base_datos || '—'}</div></div>
665
+ <div class="stack-item"><div class="si-label">Package Manager</div><div class="si-val">${config.package_manager || '—'}</div></div>
666
+ </div>
667
+ <div class="docs-h2">Commands</div>
668
+ <div class="cmd-row"><div class="cmd-label">dev</div><div class="cmd-val">${config.cmd_dev || '—'}</div></div>
669
+ <div class="cmd-row"><div class="cmd-label">test</div><div class="cmd-val">${config.cmd_test || '—'}</div></div>
670
+ <div class="cmd-row"><div class="cmd-label">build</div><div class="cmd-val">${config.cmd_build || '—'}</div></div>
671
+ </div>
672
+
673
+ <!-- COMMANDS -->
674
+ <div class="docs-section" id="doc-commands">
675
+ <div class="docs-h1">Commands Reference</div>
676
+ <div class="docs-sub">All commands available in this project.</div>
677
+ <div class="docs-h2">Development — aa:</div>
678
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">setup</div><div class="cmd-val">aa: configurar</div></div>
679
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">task</div><div class="cmd-val">aa: [your task here]</div></div>
680
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">resume</div><div class="cmd-val">aa: continúa — [answer]</div></div>
681
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">help</div><div class="cmd-val">aa: help</div></div>
682
+ <div class="docs-h2">QA Department — audit:</div>
683
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">full</div><div class="cmd-val">audit: auditar</div></div>
684
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">security</div><div class="cmd-val">audit: seguridad</div></div>
685
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">help</div><div class="cmd-val">audit: help</div></div>
686
+ <div class="docs-h2">Knowledge Graph</div>
687
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">sync</div><div class="cmd-val">node .agentic/grafo/grafo.js sync</div></div>
688
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">stats</div><div class="cmd-val">node .agentic/grafo/grafo.js stats</div></div>
689
+ <div class="cmd-row"><div class="cmd-label" style="color:var(--text2)">dashboard</div><div class="cmd-val">node dashboard-v4.js</div></div>
690
+ </div>
691
+
692
+ <!-- MODULES -->
693
+ <div class="docs-section" id="doc-modules" style="display:none;padding:0">
694
+ <div style="display:flex;height:calc(100vh - 142px);overflow:hidden">
695
+ <!-- Graph — exact same pattern as Knowledge Graph -->
696
+ <div class="graph-area" id="mod-area" style="position:relative;flex:1;overflow:hidden;background:var(--bg)">
697
+ <svg id="mod-svg" style="width:100%;height:100%"></svg>
698
+ <div class="gtt" id="mod-tt"></div>
699
+ <div style="position:absolute;top:10px;left:10px;background:rgba(17,21,32,.9);border:1px solid var(--border);border-radius:8px;padding:8px 12px;display:flex;gap:12px">
700
+ <div style="font-size:10px;color:#34d399;display:flex;align-items:center;gap:4px"><div style="width:12px;height:12px;border-radius:3px;background:rgba(16,185,129,0.2);border:1px solid rgba(16,185,129,0.5)"></div>implemented</div>
701
+ <div style="font-size:10px;color:#fbbf24;display:flex;align-items:center;gap:4px"><div style="width:12px;height:12px;border-radius:3px;background:rgba(245,158,11,0.1);border:1px solid rgba(245,158,11,0.4)"></div>pending</div>
702
+ </div>
703
+ <div style="position:absolute;bottom:12px;left:12px;display:flex;gap:6px">
704
+ <button onclick="resetModGraph()" style="background:rgba(17,21,32,.9);border:1px solid var(--border);color:var(--text2);border-radius:6px;padding:5px 9px;font-size:10px;cursor:pointer">⟳ Reset</button>
705
+ </div>
706
+ <!-- Detail panel -->
707
+ <div id="mod-detail" style="position:absolute;right:12px;top:12px;width:240px;background:var(--bg2);border:1px solid var(--border);border-radius:10px;box-shadow:0 8px 32px rgba(0,0,0,.5);display:none;padding:14px">
708
+ <div style="display:flex;justify-content:space-between;margin-bottom:8px">
709
+ <div style="font-size:13px;font-weight:600;color:var(--text)" id="mod-det-title"></div>
710
+ <div onclick="document.getElementById('mod-detail').style.display='none'" style="cursor:pointer;color:var(--text3)">×</div>
711
+ </div>
712
+ <div id="mod-det-body"></div>
713
+ </div>
714
+ </div>
715
+ <!-- List panel -->
716
+ <div style="width:220px;flex-shrink:0;background:var(--bg2);border-left:1px solid var(--border);overflow-y:auto;padding:10px">
717
+ <div style="font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;font-weight:700;margin-bottom:8px">✅ Implemented (${modulosImpl.length})</div>
718
+ ${modulosImpl.length ? modulosImpl.map(m => {
719
+ const clean = m.replace(/\*\*/g,'').replace(/✅/g,'').trim();
720
+ const area = clean.toLowerCase().split(/[\s\-\/]/)[0];
721
+ const nc = nodes.filter(n => n.area === area);
722
+ const errs = nc.filter(n => n.tipo === 'error').length;
723
+ const pats = nc.filter(n => n.tipo === 'patron').length;
724
+ return `<div onclick="selectModule('${clean.replace(/'/g,"\'")}','${area}',null)" style="padding:7px 10px;border-radius:6px;margin-bottom:4px;cursor:pointer;border:1px solid transparent;background:var(--bg3);transition:all .15s" onmouseover="this.style.borderColor='#10b981'" onmouseout="this.style.borderColor='transparent'"><div style="font-size:11px;font-weight:500;color:var(--text);margin-bottom:2px">${clean.length>24?clean.slice(0,24)+'…':clean}</div><div style="display:flex;gap:4px">${errs>0?`<span style="font-size:9px;color:#f87171">${errs} err</span>`:''}${pats>0?`<span style="font-size:9px;color:#34d399">${pats} pat</span>`:''}</div></div>`;
725
+ }).join('') : '<div style="font-size:11px;color:var(--text3);padding:8px">No modules</div>'}
726
+ <div style="font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.08em;font-weight:700;margin:12px 0 8px">⏳ Pending (${modulosPend.length})</div>
727
+ ${modulosPend.length ? modulosPend.map(m => {
728
+ const clean = m.replace(/\*\*/g,'').replace(/\[.\]\s*/g,'').trim();
729
+ return `<div style="padding:7px 10px;border-radius:6px;margin-bottom:4px;border:1px solid rgba(245,158,11,.2);background:rgba(245,158,11,.04)"><div style="font-size:11px;color:#fbbf24">${clean.length>24?clean.slice(0,24)+'…':clean}</div></div>`;
730
+ }).join('') : ''}
731
+ </div>
732
+ </div>
733
+ </div>
734
+ <!-- RULES -->
735
+ <div class="docs-section" id="doc-rules">
736
+ <div class="docs-h1">Project Rules</div>
737
+ <div class="docs-sub">Rules that apply to all development. The system enforces these automatically.</div>
738
+ ${reglas.length ? reglas.map(r => `<div class="rule-item"><div class="rule-dot"></div><div>${r}</div></div>`).join('') : '<div class="empty-state">No rules defined yet — run aa: configurar</div>'}
739
+ </div>
740
+
741
+ <!-- PATTERNS -->
742
+ <div class="docs-section" id="doc-patterns">
743
+ <div class="docs-h1">Patterns</div>
744
+ <div class="docs-sub">Rules the system learned from this project. HIGH = permanent rule applied automatically.</div>
745
+ ${patrones.length ? patrones.sort((a,b) => {const w={ALTA:3,MEDIA:2,BAJA:1}; return (w[b.confianza]||0)-(w[a.confianza]||0);}).map(p => {
746
+ const maxUse = Math.max(...patrones.map(x => x.aplicado), 1);
747
+ return `<div class="pattern-card ${p.confianza==='ALTA'?'high':''}">
748
+ <div class="pc-top">
749
+ <div class="pc-title">${p.titulo}</div>
750
+ <span class="mb c${p.confianza}">${p.confianza}</span>
751
+ <span class="ab">${p.area}</span>
752
+ </div>
753
+ ${p.aplicado > 0 ? `<div style="font-size:10px;color:var(--text3);margin-bottom:4px">Applied ${p.aplicado} times · ${p.util} useful</div><div class="usage-bar"><div class="usage-fill" style="width:${Math.round(p.aplicado/maxUse*100)}%"></div></div>` : ''}
754
+ </div>`;
755
+ }).join('') : '<div class="empty-state">No patterns yet — they build up as you work</div>'}
756
+ </div>
757
+
758
+ <!-- DECISIONS -->
759
+ <div class="docs-section" id="doc-decisions">
760
+ <div class="docs-h1">Architectural Decisions</div>
761
+ <div class="docs-sub">Why things are the way they are. The most important layer of project knowledge.</div>
762
+ ${decisiones.length ? decisiones.map(d => `<div class="decision-card"><div class="dc-title">${d.titulo}</div><div class="dc-body" style="color:var(--text3);font-size:10px;margin-bottom:4px">${d.area} · ${d.confianza}</div></div>`).join('') : '<div class="empty-state">No decisions recorded yet</div>'}
763
+ </div>
764
+
765
+ <!-- ERRORS -->
766
+ <div class="docs-section" id="doc-errors">
767
+ <div class="docs-h1">Known Error Patterns</div>
768
+ <div class="docs-sub">Errors the system has already learned to avoid automatically.</div>
769
+ ${errores.length ? errores.sort((a,b)=>b.aplicado-a.aplicado).map(e => `<div class="pattern-card" style="border-left:3px solid var(--red)">
770
+ <div class="pc-top"><div class="pc-title">${e.titulo}</div><span class="mb c${e.confianza}">${e.confianza}</span><span class="ab">${e.area}</span></div>
771
+ ${e.aplicado > 0 ? `<div style="font-size:10px;color:var(--text3)">Resolved ${e.aplicado} times</div>` : ''}
772
+ </div>`).join('') : '<div class="empty-state">No errors recorded yet</div>'}
773
+ </div>
774
+
775
+ <!-- FOR NEW DEVS -->
776
+ <div class="docs-section" id="doc-questions">
777
+ <div class="docs-h1">For New Developers</div>
778
+ <div class="docs-sub">Everything a new team member needs to get up to speed.</div>
779
+ <div class="report-section">
780
+ <div class="report-title">💡 Suggested Questions to explore</div>
781
+ ${suggestedQuestions.map(q => `<div class="question-card">${q}<span class="question-arrow">↗</span></div>`).join('')}
782
+ </div>
783
+ <div class="docs-h2">🔑 Key things to know</div>
784
+ ${patrones.filter(p=>p.confianza==='ALTA').length>0 ? `
785
+ <div style="background:rgba(16,185,129,.05);border:1px solid rgba(16,185,129,.2);border-radius:10px;padding:14px;margin-bottom:12px">
786
+ <div style="font-size:11px;font-weight:600;color:#34d399;margin-bottom:8px">★ Permanent rules (HIGH confidence)</div>
787
+ ${patrones.filter(p=>p.confianza==='ALTA').map(p=>`<div style="font-size:12px;color:var(--text2);padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)">${p.titulo}</div>`).join('')}
788
+ </div>` : ''}
789
+ ${errores.length>0 ? `
790
+ <div style="background:rgba(239,68,68,.04);border:1px solid rgba(239,68,68,.15);border-radius:10px;padding:14px;margin-bottom:12px">
791
+ <div style="font-size:11px;font-weight:600;color:#f87171;margin-bottom:8px">⚠️ Errors to avoid</div>
792
+ ${errores.slice(0,3).map(e=>`<div style="font-size:12px;color:var(--text2);padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04)">${e.titulo}</div>`).join('')}
793
+ </div>` : ''}
794
+ </div>
795
+
796
+ </div>
797
+ </div>
798
+ </div>
799
+
800
+ </div>
801
+
802
+ <script>
803
+ const NODES = ${JSON.stringify(nodes)};
804
+ const EDGES = ${JSON.stringify(edges)};
805
+ const M_NODES = ${JSON.stringify(mNodes)};
806
+ const M_EDGES = ${JSON.stringify(mEdges)};
807
+ const DEGREE_MAP = ${JSON.stringify(degreeMap)};
808
+ const MAX_DEGREE = ${maxDegree};
809
+ const GOD_THRESHOLD = ${godThreshold};
810
+ const COLORS = {error:'#ef4444',patron:'#10b981',decision:'#3b82f6'};
811
+ let lang='en', isDark=true, currentFilter='all', searchVal='', selectedNodeId=null;
812
+ let simulation, svgEl, linkSel, nodeSel, labelSel, labelsVisible=false;
813
+ let modGraphRendered=false;
814
+ const nodeMap={};
815
+ NODES.forEach(n=>nodeMap[n.id]=n);
816
+ const relMap={};
817
+ EDGES.forEach(e=>{
818
+ if(!relMap[e.desde_id])relMap[e.desde_id]=[];
819
+ if(!relMap[e.hacia_id])relMap[e.hacia_id]=[];
820
+ relMap[e.desde_id].push({...e,dir:'out'});
821
+ relMap[e.hacia_id].push({...e,dir:'in'});
822
+ });
823
+
824
+ const T={
825
+ en:{tab_graph:'Knowledge Graph',tab_docs:'Project Docs',sb_nodes:'Nodes',sb_report:'Report',sb_stats:'Stats',f_all:'All',f_err:'Errors',f_pat:'Patterns',f_dec:'Decisions',f_high:'★ HIGH',f_god:'⚡ Divine',s_total:'nodes',s_rel:'relations',s_god:'divine',s_high:'HIGH',l_err:'error',l_pat:'pattern',l_dec:'decision',l_divine:'divine',btn_reset:'⟳ Reset',btn_center:'⊙ Center',btn_labels:'Labels',nav_overview:'Overview',nav_project:'Project',nav_stack:'Stack',nav_commands:'Commands',nav_arch:'Architecture',nav_modules:'Modules',nav_rules:'Rules',nav_knowledge:'Knowledge',nav_patterns:'Patterns',nav_decisions:'Decisions',nav_errors:'Errors',nav_questions:'For New Devs',h_stack:'Tech Stack',sub_stack:'Technologies used.',graph_report:'Graph Report',divine_nodes:'Divine nodes',surprising:'Surprising connections',btn_print:'Print / Export PDF',btn_copy:'Copy as Markdown',dark:'Dark',light:'Light'},
826
+ es:{tab_graph:'Grafo de conocimiento',tab_docs:'Docs del proyecto',sb_nodes:'Nodos',sb_report:'Reporte',sb_stats:'Stats',f_all:'Todos',f_err:'Errores',f_pat:'Patrones',f_dec:'Decisiones',f_high:'★ ALTA',f_god:'⚡ Divinos',s_total:'nodos',s_rel:'relaciones',s_god:'divinos',s_high:'ALTA',l_err:'error',l_pat:'patrón',l_dec:'decisión',l_divine:'divino',btn_reset:'⟳ Resetear',btn_center:'⊙ Centrar',btn_labels:'Labels',nav_overview:'Vista general',nav_project:'Proyecto',nav_stack:'Stack',nav_commands:'Comandos',nav_arch:'Arquitectura',nav_modules:'Módulos',nav_rules:'Reglas',nav_knowledge:'Conocimiento',nav_patterns:'Patrones',nav_decisions:'Decisiones',nav_errors:'Errores',nav_questions:'Para nuevos devs',h_stack:'Stack Tecnológico',sub_stack:'Tecnologías del proyecto.',graph_report:'Reporte del grafo',divine_nodes:'Nodos divinos',surprising:'Conexiones sorprendentes',btn_print:'Imprimir / Exportar PDF',btn_copy:'Copiar como Markdown',dark:'Oscuro',light:'Claro'}
827
+ };
828
+
829
+ function setMode(mode,el){
830
+ document.querySelectorAll('.mode-tab').forEach(t=>t.classList.remove('active'));
831
+ el.classList.add('active');
832
+ document.getElementById('mode-graph').style.display=mode==='graph'?'flex':'none';
833
+ document.getElementById('mode-docs').style.display=mode==='docs'?'flex':'none';
834
+ if(mode==='docs')setTimeout(renderModuleGraph,100);
835
+ }
836
+
837
+ function showDoc(section,el){
838
+ document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
839
+ el.classList.add('active');
840
+ // Hide all sections
841
+ document.querySelectorAll('.docs-section').forEach(function(s){
842
+ s.style.display='none';
843
+ s.classList.remove('active');
844
+ });
845
+ const sec=document.getElementById('doc-'+section);
846
+ sec.style.display='block';
847
+ sec.classList.add('active');
848
+ const main=document.querySelector('.docs-main');
849
+ if(section==='modules'){
850
+ main.style.padding='0';
851
+ main.style.overflow='hidden';
852
+ sec.style.display='block';
853
+ renderModuleGraph();
854
+ } else {
855
+ main.style.padding='24px 28px';
856
+ main.style.overflow='auto';
857
+ }
858
+ }
859
+
860
+ function showSbTab(tab,el){
861
+ document.querySelectorAll('.sb-tab').forEach(t=>t.classList.remove('active'));
862
+ el.classList.add('active');
863
+ document.getElementById('sbt-nodes').style.display=tab==='nodes'?'flex':'none';
864
+ document.getElementById('sbt-report').style.display=tab==='report'?'block':'none';
865
+ document.getElementById('sbt-stats').style.display=tab==='stats'?'block':'none';
866
+ }
867
+
868
+ function setLang(l){
869
+ lang=l;
870
+ document.querySelectorAll('[data-i]').forEach(el=>{const k=el.getAttribute('data-i');if(T[l][k])el.textContent=T[l][k];});
871
+ document.getElementById('tbtn').textContent=(isDark?'🌙 ':'☀️ ')+T[l][isDark?'dark':'light'];
872
+ renderNodeList();
873
+ }
874
+
875
+ function toggleTheme(){
876
+ isDark=!isDark;
877
+ document.getElementById('app').className=isDark?'':'light';
878
+ document.getElementById('tbtn').textContent=(isDark?'🌙 ':'☀️ ')+T[lang][isDark?'dark':'light'];
879
+ }
880
+
881
+ function toggleLabels(){
882
+ labelsVisible=!labelsVisible;
883
+ document.getElementById('label-btn').textContent='Labels '+(labelsVisible?'ON':'OFF');
884
+ if(labelSel)labelSel.attr('opacity',labelsVisible?1:0);
885
+ }
886
+
887
+ function setFilter(f,el){
888
+ currentFilter=f;
889
+ document.querySelectorAll('.fpill').forEach(p=>p.classList.remove('active'));
890
+ el.classList.add('active');
891
+ renderNodeList();
892
+ highlightByFilter();
893
+ }
894
+
895
+ function filterSearch(val){searchVal=val.toLowerCase();renderNodeList();}
896
+
897
+ function getFiltered(){
898
+ let r=NODES;
899
+ if(currentFilter==='ALTA')r=NODES.filter(n=>n.confianza==='ALTA');
900
+ else if(currentFilter==='god')r=NODES.filter(n=>(DEGREE_MAP[n.id]||0)>=GOD_THRESHOLD&&GOD_THRESHOLD>0);
901
+ else if(currentFilter!=='all')r=NODES.filter(n=>n.tipo===currentFilter);
902
+ if(searchVal)r=r.filter(n=>n.titulo.toLowerCase().includes(searchVal)||n.area.toLowerCase().includes(searchVal));
903
+ return r;
904
+ }
905
+
906
+ function getConfTag(n){
907
+ const deg=DEGREE_MAP[n.id]||0;
908
+ if(deg>=GOD_THRESHOLD&&GOD_THRESHOLD>0)return '<span class="tag-ext">EXTRACTED</span>';
909
+ if(n.confianza==='ALTA')return '<span class="tag-inf">INFERRED</span>';
910
+ return '<span class="tag-amb">AMBIGUOUS</span>';
911
+ }
912
+
913
+ function renderNodeList(){
914
+ const list=document.getElementById('nodes-list');
915
+ const filtered=getFiltered();
916
+ const tl={error:T[lang].l_err,patron:T[lang].l_pat,decision:T[lang].l_dec};
917
+ if(!filtered.length){list.innerHTML='<div class="empty-state">📭 No nodes found</div>';return;}
918
+ list.innerHTML=filtered.map(n=>{
919
+ const title=n.titulo.length>48?n.titulo.slice(0,48)+'…':n.titulo;
920
+ const isGod=(DEGREE_MAP[n.id]||0)>=GOD_THRESHOLD&&GOD_THRESHOLD>0;
921
+ const deg=DEGREE_MAP[n.id]||0;
922
+ return \`<div class="nitem\${n.id===selectedNodeId?' selected':''}\${isGod?' god-node':''}" onclick="selectNode(\${n.id})" id="nitem-\${n.id}">
923
+ <div style="display:flex;align-items:center;gap:5px;margin-bottom:4px">
924
+ \${isGod?'<span style="color:var(--amber);font-size:10px">⚡</span>':''}
925
+ <span class="ntb t-\${n.tipo}">\${tl[n.tipo]||n.tipo}</span>
926
+ <span style="font-size:11px;color:var(--text);flex:1;line-height:1.3">\${title}</span>
927
+ </div>
928
+ <div style="display:flex;gap:4px;flex-wrap:wrap;align-items:center">
929
+ <span class="mb c\${n.confianza}">\${n.confianza}</span>
930
+ <span class="ab">\${n.area}</span>
931
+ \${deg>0?'<span class="ab">'+deg+' conn</span>':''}
932
+ \${n.aplicado>0?'<span class="ab">✓ '+n.aplicado+'x</span>':''}
933
+ \${getConfTag(n)}
934
+ </div>
935
+ </div>\`;
936
+ }).join('');
937
+ }
938
+
939
+ function selectNode(id){
940
+ selectedNodeId=id;
941
+ renderNodeList();
942
+ showDetail(nodeMap[id]);
943
+ focusNode(id);
944
+ const el=document.getElementById('nitem-'+id);
945
+ if(el)el.scrollIntoView({block:'nearest'});
946
+ }
947
+
948
+ function showDetail(node){
949
+ if(!node)return;
950
+ document.getElementById('dp-title').textContent=node.titulo;
951
+ const deg=DEGREE_MAP[node.id]||0;
952
+ const isGod=deg>=GOD_THRESHOLD&&GOD_THRESHOLD>0;
953
+ const rels=relMap[node.id]||[];
954
+ const relHTML=rels.map(r=>{
955
+ const other=r.dir==='out'?nodeMap[r.hacia_id]:nodeMap[r.desde_id];
956
+ if(!other)return'';
957
+ const t=other.titulo.length>30?other.titulo.slice(0,30)+'…':other.titulo;
958
+ const relLabel=r.dir==='out'?r.tipo:'← '+r.tipo;
959
+ return \`<div class="rel-item" onclick="selectNode(\${other.id})"><div style="width:7px;height:7px;border-radius:50%;background:\${COLORS[other.tipo]||'#8b5cf6'};flex-shrink:0"></div><div class="rel-name">\${t}</div><span class="rel-type-label">\${relLabel}</span></div>\`;
960
+ }).filter(Boolean).join('');
961
+ const cl=node.contenido?node.contenido.split('\\n').filter(l=>l.trim()&&!l.startsWith('##')&&!l.startsWith('Área')&&!l.startsWith('Confianza')&&!l.startsWith('Aplicado')&&!l.startsWith('Útil')&&!l.startsWith('Estado')).slice(0,5).join('\\n'):'';
962
+ const confPct=node.aplicado>0?Math.min(Math.round(node.util/node.aplicado*100),100):0;
963
+ document.getElementById('dp-body').innerHTML=\`
964
+ <div class="dp-badges">
965
+ \${isGod?'<span class="mb" style="background:rgba(245,158,11,.2);color:#fbbf24;border:1px solid rgba(245,158,11,.3)">⚡ divine</span>':''}
966
+ <span class="mb t-\${node.tipo}" style="font-size:11px;padding:3px 8px">\${node.tipo}</span>
967
+ <span class="mb c\${node.confianza}" style="font-size:11px;padding:3px 8px">\${node.confianza}</span>
968
+ <span class="ab" style="font-size:11px;padding:3px 8px">\${node.area}</span>
969
+ </div>
970
+ <div class="dp-section">
971
+ <div class="dp-label">Connections · Confidence tag</div>
972
+ <div class="dp-val">\${deg} connections · \${getConfTag(node)}</div>
973
+ </div>
974
+ <div class="dp-section">
975
+ <div class="dp-label">Applied / Useful</div>
976
+ <div class="dp-val">\${node.aplicado}x applied · \${node.util}x useful</div>
977
+ \${node.aplicado>0?'<div class="conf-progress"><div class="conf-progress-fill" style="width:'+confPct+'%;background:'+( confPct>=80?'#10b981':confPct>=50?'#f59e0b':'#ef4444')+'"></div></div>':''}
978
+ </div>
979
+ \${cl?'<div class="dp-section"><div class="dp-label">Details</div><div class="dp-val" style="font-size:10px;background:var(--bg3);border-radius:6px;padding:8px;white-space:pre-wrap;max-height:120px;overflow-y:auto">'+cl+'</div></div>':''}
980
+ \${rels.length>0?'<div class="dp-section"><div class="dp-label">Connected nodes ('+rels.length+')</div>'+relHTML+'</div>':''}
981
+ \`;
982
+ document.getElementById('detail-panel').classList.add('visible');
983
+ }
984
+
985
+ function closeDetail(){
986
+ document.getElementById('detail-panel').classList.remove('visible');
987
+ selectedNodeId=null;
988
+ renderNodeList();
989
+ if(nodeSel)nodeSel.attr('stroke',d=>getNodeStroke(d)).attr('stroke-width',d=>getNodeStrokeW(d)).attr('fill-opacity',d=>d.confianza==='ALTA'?1:0.75);
990
+ if(linkSel)linkSel.attr('stroke-opacity',0.35).attr('stroke','#2a3050').attr('stroke-width',1);
991
+ }
992
+
993
+ function focusNode(id){
994
+ if(!nodeSel)return;
995
+ nodeSel.attr('stroke',d=>d.id===id?'#fff':getNodeStroke(d))
996
+ .attr('stroke-width',d=>d.id===id?3:getNodeStrokeW(d))
997
+ .attr('fill-opacity',d=>d.id===id?1:selectedNodeId?0.2:d.confianza==='ALTA'?1:0.75);
998
+ if(linkSel){
999
+ linkSel.attr('stroke-opacity',e=>e.source.id===id||e.target.id===id?0.9:0.06)
1000
+ .attr('stroke',e=>e.source.id===id||e.target.id===id?'#a78bfa':'#2a3050')
1001
+ .attr('stroke-width',e=>e.source.id===id||e.target.id===id?2:1);
1002
+ }
1003
+ }
1004
+
1005
+ function highlightEdge(srcId,tgtId){
1006
+ if(!linkSel)return;
1007
+ linkSel.attr('stroke-opacity',e=>(e.source.id===srcId&&e.target.id===tgtId)||(e.source.id===tgtId&&e.target.id===srcId)?1:0.06)
1008
+ .attr('stroke',e=>(e.source.id===srcId&&e.target.id===tgtId)||(e.source.id===tgtId&&e.target.id===srcId)?'#ec4899':'#2a3050')
1009
+ .attr('stroke-width',e=>(e.source.id===srcId&&e.target.id===tgtId)||(e.source.id===tgtId&&e.target.id===srcId)?3:1);
1010
+ if(nodeSel)nodeSel.attr('fill-opacity',d=>d.id===srcId||d.id===tgtId?1:0.15);
1011
+ }
1012
+
1013
+ function highlightByFilter(){
1014
+ if(!nodeSel)return;
1015
+ const ids=getFiltered().map(n=>n.id);
1016
+ nodeSel.attr('fill-opacity',d=>ids.includes(d.id)?(d.confianza==='ALTA'?1:0.85):0.1);
1017
+ if(linkSel)linkSel.attr('stroke-opacity',e=>ids.includes(e.source.id)&&ids.includes(e.target.id)?0.5:0.03);
1018
+ }
1019
+
1020
+ function getNodeStroke(d){
1021
+ const deg=DEGREE_MAP[d.id]||0;
1022
+ if(deg>=GOD_THRESHOLD&&GOD_THRESHOLD>0)return '#f59e0b';
1023
+ if(d.confianza==='ALTA')return 'rgba(255,255,255,0.5)';
1024
+ return 'none';
1025
+ }
1026
+ function getNodeStrokeW(d){
1027
+ const deg=DEGREE_MAP[d.id]||0;
1028
+ if(deg>=GOD_THRESHOLD&&GOD_THRESHOLD>0)return 2.5;
1029
+ if(d.confianza==='ALTA')return 1.5;
1030
+ return 0;
1031
+ }
1032
+ function getNodeRadius(d){
1033
+ const deg=DEGREE_MAP[d.id]||0;
1034
+ const base=d.confianza==='ALTA'?13:d.confianza==='MEDIA'?10:7;
1035
+ const bonus=MAX_DEGREE>0?Math.round((deg/MAX_DEGREE)*8):0;
1036
+ return base+bonus;
1037
+ }
1038
+
1039
+ function resetGraph(){
1040
+ closeDetail();
1041
+ currentFilter='all';
1042
+ searchVal='';
1043
+ document.getElementById('srch').value='';
1044
+ document.querySelectorAll('.fpill').forEach((p,i)=>p.classList.toggle('active',i===0));
1045
+ renderNodeList();
1046
+ if(nodeSel)nodeSel.attr('fill-opacity',d=>d.confianza==='ALTA'?1:0.75).attr('stroke',d=>getNodeStroke(d)).attr('stroke-width',d=>getNodeStrokeW(d));
1047
+ if(linkSel)linkSel.attr('stroke-opacity',0.35).attr('stroke','#2a3050').attr('stroke-width',1);
1048
+ }
1049
+
1050
+ function centerGraph(){
1051
+ if(!svgEl||!simulation)return;
1052
+ const c=document.getElementById('gc');
1053
+ simulation.force('center',d3.forceCenter(c.clientWidth/2,c.clientHeight/2)).alpha(0.3).restart();
1054
+ }
1055
+
1056
+ // ─── D3 Knowledge Graph ───────────────────────────────────────
1057
+ function renderGraph(){
1058
+ if(!NODES.length)return;
1059
+ const container=document.getElementById('gc');
1060
+ const W=container.clientWidth||800,H=container.clientHeight||600;
1061
+
1062
+ svgEl=d3.select('#gc').attr('width',W).attr('height',H)
1063
+ .call(d3.zoom().scaleExtent([0.15,4]).on('zoom',ev=>g.attr('transform',ev.transform)));
1064
+
1065
+ const g=svgEl.append('g');
1066
+
1067
+ // Gradient glow for god nodes
1068
+ const defs=svgEl.append('defs');
1069
+ defs.append('radialGradient').attr('id','god-glow').attr('cx','50%').attr('cy','50%').attr('r','50%')
1070
+ .selectAll('stop').data([{o:'0%',c:'rgba(245,158,11,0.4)'},{o:'100%',c:'rgba(245,158,11,0)'}])
1071
+ .enter().append('stop').attr('offset',d=>d.o).attr('stop-color',d=>d.c);
1072
+
1073
+ defs.append('marker').attr('id','arrow').attr('viewBox','0 -4 8 8').attr('refX',20).attr('refY',0)
1074
+ .attr('markerWidth',5).attr('markerHeight',5).attr('orient','auto')
1075
+ .append('path').attr('d','M0,-4L8,0L0,4').attr('fill','#3a4060');
1076
+
1077
+ const links=EDGES.map(e=>({...e,source:e.desde_id,target:e.hacia_id})).filter(e=>nodeMap[e.source]&&nodeMap[e.target]);
1078
+
1079
+ simulation=d3.forceSimulation(NODES)
1080
+ .force('link',d3.forceLink(links).id(d=>d.id).distance(d=>{
1081
+ const sd=DEGREE_MAP[d.source.id]||0, td=DEGREE_MAP[d.target.id]||0;
1082
+ return sd>=GOD_THRESHOLD||td>=GOD_THRESHOLD?120:90;
1083
+ }))
1084
+ .force('charge',d3.forceManyBody().strength(d=>(DEGREE_MAP[d.id]||0)>=GOD_THRESHOLD?-300:-150))
1085
+ .force('center',d3.forceCenter(W/2,H/2))
1086
+ .force('collision',d3.forceCollide(d=>getNodeRadius(d)+4));
1087
+
1088
+ linkSel=g.append('g').selectAll('line').data(links).enter().append('line')
1089
+ .attr('stroke','#2a3050').attr('stroke-width',1).attr('stroke-opacity',0.35)
1090
+ .attr('marker-end','url(#arrow)');
1091
+
1092
+ // God node glow circles
1093
+ g.append('g').selectAll('circle.glow').data(NODES.filter(n=>(DEGREE_MAP[n.id]||0)>=GOD_THRESHOLD&&GOD_THRESHOLD>0)).enter().append('circle')
1094
+ .attr('class','glow').attr('r',d=>getNodeRadius(d)+8).attr('fill','url(#god-glow)').style('pointer-events','none');
1095
+
1096
+ nodeSel=g.append('g').selectAll('circle.node').data(NODES).enter().append('circle')
1097
+ .attr('class','node')
1098
+ .attr('r',d=>getNodeRadius(d))
1099
+ .attr('fill',d=>COLORS[d.tipo]||'#8b5cf6')
1100
+ .attr('fill-opacity',d=>d.confianza==='ALTA'?1:0.75)
1101
+ .attr('stroke',d=>getNodeStroke(d))
1102
+ .attr('stroke-width',d=>getNodeStrokeW(d))
1103
+ .style('cursor','pointer')
1104
+ .call(d3.drag()
1105
+ .on('start',(ev,d)=>{if(!ev.active)simulation.alphaTarget(0.3).restart();d.fx=d.x;d.fy=d.y;})
1106
+ .on('drag',(ev,d)=>{d.fx=ev.x;d.fy=ev.y;})
1107
+ .on('end',(ev,d)=>{if(!ev.active)simulation.alphaTarget(0);d.fx=null;d.fy=null;}))
1108
+ .on('click',(ev,d)=>{ev.stopPropagation();selectNode(d.id);})
1109
+ .on('mouseover',(ev,d)=>{
1110
+ const tt=document.getElementById('gtt');
1111
+ const deg=DEGREE_MAP[d.id]||0;
1112
+ const isGod=deg>=GOD_THRESHOLD&&GOD_THRESHOLD>0;
1113
+ tt.innerHTML=\`<strong style="color:var(--text)">\${isGod?'⚡ ':''}\${d.titulo.slice(0,40)}\${d.titulo.length>40?'…':''}</strong><br><span style="color:var(--text3);font-size:10px">\${d.tipo} · \${d.area} · \${d.confianza} · \${deg} connections</span>\`;
1114
+ tt.style.opacity=1;
1115
+ const r=container.getBoundingClientRect();
1116
+ tt.style.left=(ev.clientX-r.left+12)+'px';tt.style.top=(ev.clientY-r.top-10)+'px';
1117
+ })
1118
+ .on('mouseout',()=>{document.getElementById('gtt').style.opacity=0;});
1119
+
1120
+ // Labels (hidden by default, toggle with button)
1121
+ labelSel=g.append('g').selectAll('text').data(NODES.filter(n=>n.confianza==='ALTA'||(DEGREE_MAP[n.id]||0)>=GOD_THRESHOLD)).enter().append('text')
1122
+ .text(d=>d.titulo.slice(0,16)+(d.titulo.length>16?'…':''))
1123
+ .attr('font-size',9).attr('fill',d=>(DEGREE_MAP[d.id]||0)>=GOD_THRESHOLD?'rgba(245,158,11,0.8)':'rgba(255,255,255,0.4)')
1124
+ .attr('text-anchor','middle').attr('dy',d=>-(getNodeRadius(d)+5)).style('pointer-events','none')
1125
+ .attr('opacity',0);
1126
+
1127
+ // Update glow positions
1128
+ const glowSel=g.selectAll('circle.glow');
1129
+
1130
+ simulation.on('tick',()=>{
1131
+ linkSel.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
1132
+ nodeSel.attr('cx',d=>Math.max(15,Math.min(W-15,d.x))).attr('cy',d=>Math.max(15,Math.min(H-15,d.y)));
1133
+ glowSel.attr('cx',d=>Math.max(15,Math.min(W-15,d.x))).attr('cy',d=>Math.max(15,Math.min(H-15,d.y)));
1134
+ if(labelSel)labelSel.attr('x',d=>d.x).attr('y',d=>d.y);
1135
+ });
1136
+
1137
+ svgEl.on('click',()=>closeDetail());
1138
+ }
1139
+
1140
+ // ─── D3 Module Neural Graph (fullscreen) ─────────────────────
1141
+ let modSim, modNodeG, modLink2;
1142
+
1143
+ function renderModuleGraph(){
1144
+ if(modGraphRendered||(!M_NODES.length))return;
1145
+ modGraphRendered=true;
1146
+
1147
+ const container=document.getElementById('mod-area');
1148
+ if(!container)return;
1149
+ // Use docs-main width minus the list panel (220px)
1150
+ const docsMain=document.querySelector('.docs-main');
1151
+ const W=Math.max((docsMain?docsMain.clientWidth:0)-220, 400);
1152
+ const H=Math.max(window.innerHeight-160, 500);
1153
+ container.style.width=W+'px';
1154
+ container.style.height=H+'px';
1155
+
1156
+ const svg=d3.select('#mod-svg')
1157
+ .attr('width',W).attr('height',H)
1158
+ .call(d3.zoom().scaleExtent([0.2,4]).on('zoom',function(ev){g2.attr('transform',ev.transform);}));
1159
+
1160
+ const g2=svg.append('g');
1161
+ const defs2=svg.append('defs');
1162
+ defs2.append('marker').attr('id','arr2').attr('viewBox','0 -4 8 8').attr('refX',20).attr('refY',0)
1163
+ .attr('markerWidth',5).attr('markerHeight',5).attr('orient','auto')
1164
+ .append('path').attr('d','M0,-4L8,0L0,4').attr('fill','#4b5570');
1165
+
1166
+ const cleanLabel=function(s){var r=s;while(r.indexOf('**')>=0)r=r.split('**').join('');while(r.indexOf('*')>=0)r=r.split('*').join('');r=r.replace(/^\[.\]\s*/,'');return r.trim();};
1167
+ const implNodes=M_NODES.filter(function(n){return n.tipo==='impl';}).map(function(n){return Object.assign({},n,{label:cleanLabel(n.label)});});
1168
+ const pendNodes=M_NODES.filter(function(n){return n.tipo==='pend';}).map(function(n){return Object.assign({},n,{label:cleanLabel(n.label)});});
1169
+
1170
+ // Grid positions — impl top, pending bottom
1171
+ var NW=160,NH=52,HGAP=24,VGAP=32;
1172
+ var cols=Math.min(3,implNodes.length);
1173
+ if(cols===0)cols=1;
1174
+ var rows=Math.ceil(implNodes.length/cols);
1175
+ var gridW=cols*(NW+HGAP)-HGAP;
1176
+ var sx=(W-gridW)/2;
1177
+ var sy=50;
1178
+ implNodes.forEach(function(n,i){
1179
+ n.x=sx+(i%cols)*(NW+HGAP)+NW/2;
1180
+ n.y=sy+Math.floor(i/cols)*(NH+VGAP)+NH/2;
1181
+ });
1182
+ var implBottom=sy+rows*(NH+VGAP)+16;
1183
+ var PW=130,PH=40,PGAP=16;
1184
+ var pendTW=pendNodes.length*(PW+PGAP)-PGAP;
1185
+ var px=(W-pendTW)/2;
1186
+ var py=Math.max(implBottom+50, H-PH-30);
1187
+ pendNodes.forEach(function(n,i){
1188
+ n.x=px+i*(PW+PGAP)+PW/2;
1189
+ n.y=py;
1190
+ });
1191
+
1192
+ // Links: impl sequential flow
1193
+ var links=[];
1194
+ for(var i=0;i<implNodes.length-1;i++) links.push({s:implNodes[i],t:implNodes[i+1]});
1195
+
1196
+ g2.append('g').selectAll('line').data(links).enter().append('line')
1197
+ .attr('x1',function(d){return d.s.x;}).attr('y1',function(d){return d.s.y;})
1198
+ .attr('x2',function(d){return d.t.x;}).attr('y2',function(d){return d.t.y;})
1199
+ .attr('stroke','#2a3050').attr('stroke-width',1.5).attr('stroke-opacity',0.4)
1200
+ .attr('marker-end','url(#arr2)');
1201
+
1202
+ // Section labels
1203
+ g2.append('text').text('✅ Implemented').attr('x',W/2).attr('y',sy-16)
1204
+ .attr('text-anchor','middle').attr('font-size',11).attr('fill','rgba(16,185,129,0.6)');
1205
+ if(pendNodes.length>0){
1206
+ g2.append('line').attr('x1',40).attr('y1',implBottom+20).attr('x2',W-40).attr('y2',implBottom+20)
1207
+ .attr('stroke','rgba(245,158,11,0.2)').attr('stroke-width',1).attr('stroke-dasharray','4,4');
1208
+ g2.append('text').text('⏳ Pending').attr('x',W/2).attr('y',implBottom+36)
1209
+ .attr('text-anchor','middle').attr('font-size',11).attr('fill','rgba(245,158,11,0.6)');
1210
+ }
1211
+
1212
+ // Impl node groups
1213
+ var iG=g2.append('g').selectAll('g').data(implNodes).enter().append('g')
1214
+ .attr('transform',function(d){return 'translate('+d.x+','+d.y+')';})
1215
+ .style('cursor','pointer')
1216
+ .on('click',function(ev,d){ev.stopPropagation();selectModule(d.label,d.area,d);})
1217
+ .on('mouseover',function(ev,d){
1218
+ var tt=document.getElementById('mod-tt');
1219
+ tt.innerHTML='<strong style="color:var(--text)">'+d.label+'</strong><br><span style="color:var(--text3);font-size:10px">'+d.errors+' errors · '+d.patterns+' patterns</span>';
1220
+ tt.style.opacity=1;
1221
+ var r=container.getBoundingClientRect();
1222
+ tt.style.left=(ev.clientX-r.left+12)+'px';tt.style.top=(ev.clientY-r.top-8)+'px';
1223
+ }).on('mouseout',function(){document.getElementById('mod-tt').style.opacity=0;});
1224
+
1225
+ iG.append('rect').attr('width',NW).attr('height',NH).attr('x',-NW/2).attr('y',-NH/2).attr('rx',10)
1226
+ .attr('fill','rgba(16,185,129,0.1)')
1227
+ .attr('stroke',function(d){return d.degree>2?'rgba(139,92,246,0.7)':'rgba(16,185,129,0.45)';})
1228
+ .attr('stroke-width',function(d){return d.degree>2?2:1.5;});
1229
+
1230
+ iG.append('text').text(function(d){return d.label.length>17?d.label.slice(0,17)+'…':d.label;})
1231
+ .attr('text-anchor','middle').attr('dy',-4)
1232
+ .attr('font-size',12).attr('font-weight','600')
1233
+ .attr('fill',function(d){return d.degree>2?'#a78bfa':'#34d399';});
1234
+
1235
+ iG.append('text').text(function(d){
1236
+ var p=[];if(d.errors>0)p.push(d.errors+' err');if(d.patterns>0)p.push(d.patterns+' pat');return p.join(' · ')||'✓';
1237
+ }).attr('text-anchor','middle').attr('dy',14).attr('font-size',9)
1238
+ .attr('fill',function(d){return d.errors>0?'#f87171':'#6ee7b7';});
1239
+
1240
+ // Pending node groups
1241
+ if(pendNodes.length>0){
1242
+ var pG=g2.append('g').selectAll('g').data(pendNodes).enter().append('g')
1243
+ .attr('transform',function(d){return 'translate('+d.x+','+d.y+')';})
1244
+ .style('cursor','pointer')
1245
+ .on('click',function(ev,d){ev.stopPropagation();selectModule(d.label,d.area,d);})
1246
+ .on('mouseover',function(ev,d){
1247
+ var tt=document.getElementById('mod-tt');
1248
+ tt.innerHTML='<strong style="color:var(--text)">⏳ '+d.label+'</strong>';
1249
+ tt.style.opacity=1;
1250
+ var r=container.getBoundingClientRect();
1251
+ tt.style.left=(ev.clientX-r.left+12)+'px';tt.style.top=(ev.clientY-r.top-8)+'px';
1252
+ }).on('mouseout',function(){document.getElementById('mod-tt').style.opacity=0;});
1253
+
1254
+ pG.append('rect').attr('width',PW).attr('height',PH).attr('x',-PW/2).attr('y',-PH/2).attr('rx',8)
1255
+ .attr('fill','rgba(245,158,11,0.07)').attr('stroke','rgba(245,158,11,0.4)').attr('stroke-width',1.5);
1256
+
1257
+ pG.append('text').text(function(d){return d.label.length>15?d.label.slice(0,15)+'…':d.label;})
1258
+ .attr('text-anchor','middle').attr('dy',4)
1259
+ .attr('font-size',11).attr('font-weight','500').attr('fill','#fbbf24');
1260
+ }
1261
+
1262
+ svg.on('click',function(){document.getElementById('mod-detail').style.display='none';});
1263
+ }
1264
+
1265
+
1266
+ function showModTT(ev,d,container){
1267
+ var tt=document.getElementById('gtt');
1268
+ var icon=d.tipo==='impl'?'✅':'⏳';
1269
+ tt.innerHTML='<strong style="color:var(--text)">'+icon+' '+d.label+'</strong><br><span style="color:var(--text3);font-size:10px">'+d.errors+' errors · '+d.patterns+' patterns</span>';
1270
+ tt.style.opacity=1;
1271
+ var r=container.getBoundingClientRect();
1272
+ tt.style.left=(ev.clientX-r.left+14)+'px';
1273
+ tt.style.top=(ev.clientY-r.top-10)+'px';
1274
+ }
1275
+
1276
+ function getModW(d){ return Math.max(100,Math.min(d.label.length,18)*8+24); }
1277
+
1278
+ function selectModule(label,area,d){
1279
+ var panel=document.getElementById('mod-detail');
1280
+ document.getElementById('mod-det-title').textContent=label;
1281
+ var knNodes=NODES.filter(function(n){return n.area===area||n.area==='global';});
1282
+ var errs=knNodes.filter(function(n){return n.tipo==='error';});
1283
+ var pats=knNodes.filter(function(n){return n.tipo==='patron'&&n.confianza==='ALTA';});
1284
+ var decs=knNodes.filter(function(n){return n.tipo==='decision';});
1285
+ var html='';
1286
+ html+='<div style="font-size:10px;color:var(--text3);margin-bottom:8px">'+(d?d.tipo==='impl'?'Done':'Pending':'')+'</div>';
1287
+ if(errs.length>0){
1288
+ html+='<div style="margin-bottom:8px"><div style="font-size:10px;color:#f87171;font-weight:600;margin-bottom:4px">Errors ('+errs.length+')</div>';
1289
+ errs.slice(0,3).forEach(function(e){html+='<div style="font-size:11px;color:#94a3b8;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.05)">'+e.titulo.slice(0,42)+'</div>';});
1290
+ html+='</div>';
1291
+ }
1292
+ if(pats.length>0){
1293
+ html+='<div style="margin-bottom:8px"><div style="font-size:10px;color:#34d399;font-weight:600;margin-bottom:4px">HIGH patterns ('+pats.length+')</div>';
1294
+ pats.slice(0,3).forEach(function(p){html+='<div style="font-size:11px;color:#94a3b8;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.05)">'+p.titulo.slice(0,42)+'</div>';});
1295
+ html+='</div>';
1296
+ }
1297
+ if(decs.length>0){
1298
+ html+='<div><div style="font-size:10px;color:#60a5fa;font-weight:600;margin-bottom:4px">Decisions ('+decs.length+')</div>';
1299
+ decs.slice(0,2).forEach(function(dec){html+='<div style="font-size:11px;color:#94a3b8;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.05)">'+dec.titulo.slice(0,42)+'</div>';});
1300
+ html+='</div>';
1301
+ }
1302
+ if(!errs.length&&!pats.length&&!decs.length) html+='<div style="font-size:11px;color:#64748b">No knowledge recorded yet.</div>';
1303
+ document.getElementById('mod-det-body').innerHTML=html;
1304
+ panel.style.display='block';
1305
+ }
1306
+
1307
+ function resetModGraph(){
1308
+ document.getElementById('mod-detail').style.display='none';
1309
+ }
1310
+
1311
+ function centerModGraph(){
1312
+ // noop — static graph
1313
+ }
1314
+
1315
+ function copyMarkdown(){
1316
+ const t='# '+('${config.nombre}')+'\\n\\nGenerated by Agentic KDD Dashboard\\n';
1317
+ navigator.clipboard?.writeText(t).then(()=>alert('Copied!')).catch(()=>alert('Copy manually'));
1318
+ }
1319
+
1320
+ // Init
1321
+ renderNodeList();
1322
+ renderGraph();
1323
+ </script>
1324
+ </body>
1325
+ </html>`;
1326
+
1327
+ const server = http.createServer((req, res) => {
1328
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1329
+ res.end(HTML);
1330
+ });
1331
+
1332
+ server.listen(PORT, () => {
1333
+ const url = `http://localhost:${PORT}`;
1334
+ console.log('\n \x1b[34mAgentic KDD Dashboard v4\x1b[0m');
1335
+ console.log(` Project: ${config.nombre}`);
1336
+ console.log(` Nodes: ${stats.total} | Divine: ${stats.godNodes} | Surprising: ${stats.surprising} | HIGH: ${stats.high}`);
1337
+ console.log(`\n \x1b[36m→ ${url}\x1b[0m\n`);
1338
+ console.log(' Ctrl+C to stop\n');
1339
+ try {
1340
+ const cmd = process.platform === 'win32' ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`;
1341
+ require('child_process').execSync(cmd, { stdio: 'ignore' });
1342
+ } catch {}
1343
+ });
1344
+
1345
+ process.on('SIGINT', () => { server.close(); console.log('\n Stopped.\n'); process.exit(0); });