edhindex 1.0.0 → 1.0.2

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.
Files changed (199) hide show
  1. package/README.md +228 -0
  2. package/dist/cli/commands/config_cmd.d.ts +2 -0
  3. package/dist/cli/commands/config_cmd.d.ts.map +1 -0
  4. package/dist/cli/commands/config_cmd.js +53 -0
  5. package/dist/cli/commands/config_cmd.js.map +1 -0
  6. package/dist/cli/commands/doctor_cmd.d.ts +2 -0
  7. package/dist/cli/commands/doctor_cmd.d.ts.map +1 -0
  8. package/dist/cli/commands/doctor_cmd.js +32 -0
  9. package/dist/cli/commands/doctor_cmd.js.map +1 -0
  10. package/dist/cli/commands/graph.d.ts +2 -0
  11. package/dist/cli/commands/graph.d.ts.map +1 -0
  12. package/dist/cli/commands/graph.js +26 -0
  13. package/dist/cli/commands/graph.js.map +1 -0
  14. package/dist/cli/commands/index_cmd.d.ts +2 -0
  15. package/dist/cli/commands/index_cmd.d.ts.map +1 -0
  16. package/dist/cli/commands/index_cmd.js +110 -0
  17. package/dist/cli/commands/index_cmd.js.map +1 -0
  18. package/dist/cli/commands/init.d.ts +2 -0
  19. package/dist/cli/commands/init.d.ts.map +1 -0
  20. package/dist/cli/commands/init.js +53 -0
  21. package/dist/cli/commands/init.js.map +1 -0
  22. package/dist/cli/commands/knowledge-graph.d.ts +8 -0
  23. package/dist/cli/commands/knowledge-graph.d.ts.map +1 -0
  24. package/dist/cli/commands/knowledge-graph.js +76 -0
  25. package/dist/cli/commands/knowledge-graph.js.map +1 -0
  26. package/dist/cli/commands/models.d.ts +2 -0
  27. package/dist/cli/commands/models.d.ts.map +1 -0
  28. package/dist/cli/commands/models.js +17 -0
  29. package/dist/cli/commands/models.js.map +1 -0
  30. package/dist/cli/commands/reset.d.ts +2 -0
  31. package/dist/cli/commands/reset.d.ts.map +1 -0
  32. package/dist/cli/commands/reset.js +35 -0
  33. package/dist/cli/commands/reset.js.map +1 -0
  34. package/dist/cli/commands/search.d.ts +2 -0
  35. package/dist/cli/commands/search.d.ts.map +1 -0
  36. package/dist/cli/commands/search.js +40 -0
  37. package/dist/cli/commands/search.js.map +1 -0
  38. package/dist/cli/commands/start.d.ts +2 -0
  39. package/dist/cli/commands/start.d.ts.map +1 -0
  40. package/dist/cli/commands/start.js +265 -0
  41. package/dist/cli/commands/start.js.map +1 -0
  42. package/dist/cli/commands/status.d.ts +2 -0
  43. package/dist/cli/commands/status.d.ts.map +1 -0
  44. package/dist/cli/commands/status.js +47 -0
  45. package/dist/cli/commands/status.js.map +1 -0
  46. package/dist/cli/index.d.ts +3 -0
  47. package/dist/cli/index.d.ts.map +1 -0
  48. package/dist/cli/index.js +121 -0
  49. package/dist/cli/index.js.map +1 -0
  50. package/dist/config/index.d.ts +28 -0
  51. package/dist/config/index.d.ts.map +1 -0
  52. package/dist/config/index.js +65 -0
  53. package/dist/config/index.js.map +1 -0
  54. package/dist/doctor/index.d.ts +8 -0
  55. package/dist/doctor/index.d.ts.map +1 -0
  56. package/dist/doctor/index.js +117 -0
  57. package/dist/doctor/index.js.map +1 -0
  58. package/dist/embeddings/provider.d.ts +9 -0
  59. package/dist/embeddings/provider.d.ts.map +1 -0
  60. package/dist/embeddings/provider.js +2 -0
  61. package/dist/embeddings/provider.js.map +1 -0
  62. package/dist/embeddings/transformers.d.ts +26 -0
  63. package/dist/embeddings/transformers.d.ts.map +1 -0
  64. package/dist/embeddings/transformers.js +120 -0
  65. package/dist/embeddings/transformers.js.map +1 -0
  66. package/dist/graph/build.d.ts +13 -0
  67. package/dist/graph/build.d.ts.map +1 -0
  68. package/dist/graph/build.js +33 -0
  69. package/dist/graph/build.js.map +1 -0
  70. package/dist/graph/builder.d.ts +24 -0
  71. package/dist/graph/builder.d.ts.map +1 -0
  72. package/dist/graph/builder.js +231 -0
  73. package/dist/graph/builder.js.map +1 -0
  74. package/dist/graph/render.d.ts +3 -0
  75. package/dist/graph/render.d.ts.map +1 -0
  76. package/dist/graph/render.js +60 -0
  77. package/dist/graph/render.js.map +1 -0
  78. package/dist/graph/serve.d.ts +6 -0
  79. package/dist/graph/serve.d.ts.map +1 -0
  80. package/dist/graph/serve.js +389 -0
  81. package/dist/graph/serve.js.map +1 -0
  82. package/dist/graph/service.d.ts +29 -0
  83. package/dist/graph/service.d.ts.map +1 -0
  84. package/dist/graph/service.js +73 -0
  85. package/dist/graph/service.js.map +1 -0
  86. package/dist/graph/storage.d.ts +33 -0
  87. package/dist/graph/storage.d.ts.map +1 -0
  88. package/dist/graph/storage.js +164 -0
  89. package/dist/graph/storage.js.map +1 -0
  90. package/dist/graph/svg.d.ts +27 -0
  91. package/dist/graph/svg.d.ts.map +1 -0
  92. package/dist/graph/svg.js +212 -0
  93. package/dist/graph/svg.js.map +1 -0
  94. package/dist/graph/types.d.ts +34 -0
  95. package/dist/graph/types.d.ts.map +1 -0
  96. package/dist/graph/types.js +9 -0
  97. package/dist/graph/types.js.map +1 -0
  98. package/dist/indexer/chunker.d.ts +18 -0
  99. package/dist/indexer/chunker.d.ts.map +1 -0
  100. package/dist/indexer/chunker.js +139 -0
  101. package/dist/indexer/chunker.js.map +1 -0
  102. package/dist/indexer/file-utils.d.ts +17 -0
  103. package/dist/indexer/file-utils.d.ts.map +1 -0
  104. package/dist/indexer/file-utils.js +123 -0
  105. package/dist/indexer/file-utils.js.map +1 -0
  106. package/dist/indexer/ignore.d.ts +5 -0
  107. package/dist/indexer/ignore.d.ts.map +1 -0
  108. package/dist/indexer/ignore.js +96 -0
  109. package/dist/indexer/ignore.js.map +1 -0
  110. package/dist/indexer/incremental.d.ts +16 -0
  111. package/dist/indexer/incremental.d.ts.map +1 -0
  112. package/dist/indexer/incremental.js +55 -0
  113. package/dist/indexer/incremental.js.map +1 -0
  114. package/dist/indexer/indexer.d.ts +36 -0
  115. package/dist/indexer/indexer.d.ts.map +1 -0
  116. package/dist/indexer/indexer.js +121 -0
  117. package/dist/indexer/indexer.js.map +1 -0
  118. package/dist/indexer/parser.d.ts +30 -0
  119. package/dist/indexer/parser.d.ts.map +1 -0
  120. package/dist/indexer/parser.js +225 -0
  121. package/dist/indexer/parser.js.map +1 -0
  122. package/dist/logging.d.ts +15 -0
  123. package/dist/logging.d.ts.map +1 -0
  124. package/dist/logging.js +42 -0
  125. package/dist/logging.js.map +1 -0
  126. package/dist/mcp/server.d.ts +4 -0
  127. package/dist/mcp/server.d.ts.map +1 -0
  128. package/dist/mcp/server.js +226 -0
  129. package/dist/mcp/server.js.map +1 -0
  130. package/dist/mcp-config.d.ts +4 -0
  131. package/dist/mcp-config.d.ts.map +1 -0
  132. package/dist/mcp-config.js +175 -0
  133. package/dist/mcp-config.js.map +1 -0
  134. package/dist/progress.d.ts +12 -0
  135. package/dist/progress.d.ts.map +1 -0
  136. package/dist/progress.js +16 -0
  137. package/dist/progress.js.map +1 -0
  138. package/dist/retrieval/bm25.d.ts +6 -0
  139. package/dist/retrieval/bm25.d.ts.map +1 -0
  140. package/dist/retrieval/bm25.js +20 -0
  141. package/dist/retrieval/bm25.js.map +1 -0
  142. package/dist/retrieval/reranker.d.ts +11 -0
  143. package/dist/retrieval/reranker.d.ts.map +1 -0
  144. package/dist/retrieval/reranker.js +46 -0
  145. package/dist/retrieval/reranker.js.map +1 -0
  146. package/dist/retrieval/search.d.ts +35 -0
  147. package/dist/retrieval/search.d.ts.map +1 -0
  148. package/dist/retrieval/search.js +95 -0
  149. package/dist/retrieval/search.js.map +1 -0
  150. package/dist/security.d.ts +5 -0
  151. package/dist/security.d.ts.map +1 -0
  152. package/dist/security.js +42 -0
  153. package/dist/security.js.map +1 -0
  154. package/dist/storage/fts.d.ts +30 -0
  155. package/dist/storage/fts.d.ts.map +1 -0
  156. package/dist/storage/fts.js +107 -0
  157. package/dist/storage/fts.js.map +1 -0
  158. package/dist/storage/metadata.d.ts +46 -0
  159. package/dist/storage/metadata.d.ts.map +1 -0
  160. package/dist/storage/metadata.js +133 -0
  161. package/dist/storage/metadata.js.map +1 -0
  162. package/dist/vector/lancedb.d.ts +25 -0
  163. package/dist/vector/lancedb.d.ts.map +1 -0
  164. package/dist/vector/lancedb.js +93 -0
  165. package/dist/vector/lancedb.js.map +1 -0
  166. package/dist/vector/store.d.ts +16 -0
  167. package/dist/vector/store.d.ts.map +1 -0
  168. package/dist/vector/store.js +2 -0
  169. package/dist/vector/store.js.map +1 -0
  170. package/dist/version.d.ts +12 -0
  171. package/dist/version.d.ts.map +1 -0
  172. package/dist/version.js +46 -0
  173. package/dist/version.js.map +1 -0
  174. package/dist/watcher/index.d.ts +15 -0
  175. package/dist/watcher/index.d.ts.map +1 -0
  176. package/dist/watcher/index.js +65 -0
  177. package/dist/watcher/index.js.map +1 -0
  178. package/package.json +35 -4
  179. package/src/cli/commands/graph.ts +30 -0
  180. package/src/cli/commands/init.ts +41 -3
  181. package/src/cli/commands/knowledge-graph.ts +85 -0
  182. package/src/cli/commands/reset.ts +40 -0
  183. package/src/cli/commands/start.ts +54 -21
  184. package/src/cli/index.ts +33 -0
  185. package/src/config/index.ts +13 -0
  186. package/src/graph/build.ts +45 -0
  187. package/src/graph/builder.ts +243 -0
  188. package/src/graph/render.ts +67 -0
  189. package/src/graph/serve.ts +399 -0
  190. package/src/graph/service.ts +84 -0
  191. package/src/graph/storage.ts +181 -0
  192. package/src/graph/svg.ts +241 -0
  193. package/src/graph/types.ts +68 -0
  194. package/src/mcp/server.ts +212 -85
  195. package/src/mcp-config.ts +185 -0
  196. package/src/storage/metadata.ts +9 -1
  197. package/.edhindexignore +0 -66
  198. package/BUILD_BRIEF.md +0 -228
  199. package/tsconfig.json +0 -21
@@ -0,0 +1,243 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { MetadataStore } from '../storage/metadata.js';
4
+ import { GraphStore } from './storage.js';
5
+ import type { GraphNode, GraphEdge, NodeType, EdgeType } from './types.js';
6
+ import { logger } from '../logging.js';
7
+
8
+ export class GraphBuilder {
9
+ private rootPath: string;
10
+ private meta: MetadataStore;
11
+ private graph: GraphStore;
12
+ private nodeIds = new Set<string>();
13
+ private edgeIds = new Set<string>();
14
+
15
+ constructor(rootPath: string, meta: MetadataStore, graph: GraphStore) {
16
+ this.rootPath = rootPath;
17
+ this.meta = meta;
18
+ this.graph = graph;
19
+ }
20
+
21
+ buildFull() {
22
+ logger.info('Building knowledge graph...');
23
+ this.graph.clear();
24
+ this.nodeIds.clear();
25
+ this.edgeIds.clear();
26
+
27
+ this.addWorkspaceNode();
28
+ this.addFolderNodes();
29
+ this.addFileAndSymbolNodes();
30
+ this.addImportExportEdges();
31
+
32
+ logger.info(`Graph built: ${this.graph.getNodeCount()} nodes, ${this.graph.getEdgeCount()} edges`);
33
+ }
34
+
35
+ updateFile(filePath: string) {
36
+ const relPath = relative(this.rootPath, filePath);
37
+
38
+ // Remove existing nodes/edges for this file
39
+ this.graph.deleteEdgesForFile(relPath);
40
+ this.graph.deleteNodesForFile(relPath);
41
+
42
+ // Re-add nodes for this file
43
+ const chunks = this.meta.getChunksForFile(relPath);
44
+ if (chunks.length === 0) return;
45
+
46
+ const symbolsInFile = new Set(chunks.filter(c => c.symbol).map(c => c.symbol));
47
+
48
+ const fileNode = this.makeNode(`file:${relPath}`, relPath, 'file', chunks[0].language, relPath, 0, 0);
49
+ this.addEdge(`contains:file:${relPath}`, `workspace:${this.nodeId('workspace')}`, fileNode.id, 'contains');
50
+ this.graph.upsertNode(fileNode);
51
+
52
+ for (const chunk of chunks) {
53
+ if (!chunk.symbol) continue;
54
+ const nodeType = this.mapKind(chunk.kind);
55
+ const nid = `sym:${chunk.file}:${chunk.symbol}`;
56
+ const node = this.makeNode(nid, chunk.symbol, nodeType, chunk.language, chunk.file, chunk.start_line, chunk.end_line, { parent: chunk.parent || undefined });
57
+ this.graph.upsertNode(node);
58
+ this.addEdge(`defines:${relPath}:${chunk.symbol}`, fileNode.id, nid, 'defines');
59
+
60
+ if (chunk.parent && symbolsInFile.has(chunk.parent)) {
61
+ const parentId = `sym:${chunk.file}:${chunk.parent}`;
62
+ this.addEdge(`contains:sym:${chunk.file}:${chunk.parent}:${chunk.symbol}`, parentId, nid, 'contains');
63
+ }
64
+ }
65
+
66
+ this.addImportExportEdgesForFile(relPath, chunks);
67
+ }
68
+
69
+ private addWorkspaceNode() {
70
+ const wid = this.nodeId('workspace');
71
+ const node = this.makeNode(wid, this.rootPath.split(sep).pop() || 'workspace', 'workspace', '', '', 0, 0);
72
+ this.graph.upsertNode(node);
73
+ }
74
+
75
+ private addFolderNodes() {
76
+ const walk = (dir: string) => {
77
+ try {
78
+ const entries = readdirSync(dir);
79
+ for (const entry of entries) {
80
+ if (entry.startsWith('.') || entry === 'node_modules') continue;
81
+ const full = join(dir, entry);
82
+ const stat = statSync(full);
83
+ if (stat.isDirectory()) {
84
+ const relPath = relative(this.rootPath, full);
85
+ const fid = this.nodeId(`folder:${relPath}`);
86
+ const node = this.makeNode(fid, entry, 'folder', '', relPath, 0, 0);
87
+ this.graph.upsertNode(node);
88
+
89
+ const parentRel = relative(this.rootPath, join(full, '..'));
90
+ const parentId = parentRel === '' ? this.nodeId('workspace') : this.nodeId(`folder:${parentRel}`);
91
+ this.addEdge(`contains:folder:${relPath}`, parentId, fid, 'contains');
92
+
93
+ walk(full);
94
+ }
95
+ }
96
+ } catch { /* skip unreadable dirs */ }
97
+ };
98
+ walk(this.rootPath);
99
+ }
100
+
101
+ private addFileAndSymbolNodes() {
102
+ const files = this.meta.getAllFilesWithImports();
103
+ for (const f of files) {
104
+ const fid = this.nodeId(`file:${f.file}`);
105
+ const fileNode = this.makeNode(fid, f.file.split('/').pop() || f.file, 'file', f.language, f.file, 0, 0);
106
+ this.graph.upsertNode(fileNode);
107
+
108
+ const parentRel = f.file.includes('/') ? f.file.slice(0, f.file.lastIndexOf('/')) : '';
109
+ const parentId = parentRel ? this.nodeId(`folder:${parentRel}`) : this.nodeId('workspace');
110
+ this.addEdge(`contains:file:${f.file}`, parentId, fid, 'contains');
111
+
112
+ const chunks = this.meta.getChunksForFile(f.file);
113
+ const symbolsInFile = new Set<string>();
114
+ for (const chunk of chunks) {
115
+ if (chunk.symbol) symbolsInFile.add(chunk.symbol);
116
+ }
117
+ for (const chunk of chunks) {
118
+ if (!chunk.symbol) continue;
119
+ const nodeType = this.mapKind(chunk.kind);
120
+ const nid = this.nodeId(`sym:${chunk.file}:${chunk.symbol}`);
121
+ const node = this.makeNode(nid, chunk.symbol, nodeType, chunk.language, chunk.file, chunk.start_line, chunk.end_line, {
122
+ parent: symbolsInFile.has(chunk.parent || '') ? chunk.parent : undefined,
123
+ hash: chunk.hash,
124
+ });
125
+ this.graph.upsertNode(node);
126
+ this.addEdge(`defines:${f.file}:${chunk.symbol}`, fid, nid, 'defines');
127
+
128
+ if (chunk.parent && symbolsInFile.has(chunk.parent)) {
129
+ const parentId = this.nodeId(`sym:${chunk.file}:${chunk.parent}`);
130
+ this.addEdge(`contains:${chunk.file}:${chunk.parent}:${chunk.symbol}`, parentId, nid, 'contains');
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ private addImportExportEdges() {
137
+ const files = this.meta.getAllFilesWithImports();
138
+ const indexedFiles = new Set(files.map(f => f.file));
139
+ for (const f of files) {
140
+ const chunks = this.meta.getChunksForFile(f.file);
141
+ this.addImportExportEdgesForFile(f.file, chunks, indexedFiles);
142
+ }
143
+ }
144
+
145
+ private addImportExportEdgesForFile(file: string, chunks: { symbol: string; imports: string; exports: string }[], indexedFiles?: Set<string>) {
146
+ if (!indexedFiles) {
147
+ indexedFiles = new Set(this.meta.getAllFilesWithImports().map(f => f.file));
148
+ }
149
+
150
+ const fid = this.nodeId(`file:${file}`);
151
+
152
+ for (const chunk of chunks) {
153
+ // Import edges
154
+ const importModules = this.parseImportPaths(chunk.imports);
155
+ for (const imp of importModules) {
156
+ const resolved = this.resolveImport(file, imp, indexedFiles);
157
+ if (resolved) {
158
+ const tid = this.nodeId(`file:${resolved}`);
159
+ this.addEdge(`imports:${file}:${resolved}`, fid, tid, 'imports');
160
+ }
161
+ }
162
+
163
+ // Export edges
164
+ const exportModules = this.parseImportPaths(chunk.exports);
165
+ for (const exp of exportModules) {
166
+ const resolved = this.resolveImport(file, exp, indexedFiles);
167
+ if (resolved) {
168
+ const tid = this.nodeId(`file:${resolved}`);
169
+ this.addEdge(`exports:${file}:${resolved}`, fid, tid, 'exports');
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ private parseImportPaths(raw: string): string[] {
176
+ if (!raw) return [];
177
+ const results: string[] = [];
178
+ for (const line of raw.split('\n')) {
179
+ const t = line.trim();
180
+ let m = t.match(/from\s+['"](\..*?)['"]/);
181
+ if (!m) m = t.match(/require\s*\(\s*['"](\..*?)['"]\s*\)/);
182
+ if (!m) m = t.match(/import\s+['"](\..*?)['"]/);
183
+ if (m) results.push(m[1]);
184
+ }
185
+ return results;
186
+ }
187
+
188
+ private resolveImport(sourceFile: string, importPath: string, indexedFiles: Set<string>): string | null {
189
+ const raw = importPath.replace(/^['"]|['"]$/g, '');
190
+ const dir = sourceFile.includes('/') ? sourceFile.slice(0, sourceFile.lastIndexOf('/')) : '';
191
+
192
+ const normalize = (base: string, rel: string): string => {
193
+ const parts = (base ? base.split('/') : []).concat(rel.split('/'));
194
+ const out: string[] = [];
195
+ for (const p of parts) {
196
+ if (p === '.' || p === '') continue;
197
+ if (p === '..') { out.pop(); continue; }
198
+ out.push(p);
199
+ }
200
+ return out.join('/');
201
+ };
202
+
203
+ const full = normalize(dir, raw);
204
+ const candidates = [full];
205
+ for (const jsExt of ['.js', '.jsx']) {
206
+ if (full.endsWith(jsExt)) candidates.push(full.replace(jsExt, ''), full.replace(jsExt, '.ts'), full.replace(jsExt, '.tsx'));
207
+ }
208
+
209
+ for (const c of candidates) { if (indexedFiles.has(c)) return c; }
210
+ for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.py', '.go']) { if (indexedFiles.has(full + ext)) return full + ext; }
211
+ for (const idx of ['index.ts', 'index.tsx', 'index.js', 'index.jsx', '__init__.py']) { if (indexedFiles.has(full + '/' + idx)) return full + '/' + idx; }
212
+ return null;
213
+ }
214
+
215
+ private mapKind(kind: string | null): NodeType {
216
+ switch (kind) {
217
+ case 'class': return 'class';
218
+ case 'interface': return 'interface';
219
+ case 'enum': return 'enum';
220
+ case 'function': return 'function';
221
+ case 'method': return 'method';
222
+ case 'variable': return 'variable';
223
+ case 'module': return 'module';
224
+ default: return 'module';
225
+ }
226
+ }
227
+
228
+ private makeNode(id: string, label: string, type: NodeType, language: string, file: string, startLine: number, endLine: number, extra?: Record<string, unknown>): GraphNode {
229
+ return { id, label, type, language, file, startLine, endLine, metadata: extra || {} };
230
+ }
231
+
232
+ private addEdge(id: string, source: string, target: string, type: EdgeType) {
233
+ if (this.edgeIds.has(id)) return;
234
+ this.edgeIds.add(id);
235
+ this.graph.upsertEdge({ id, source, target, type, metadata: {} });
236
+ }
237
+
238
+ private nodeId(key: string): string {
239
+ const id = `kg:${key}`;
240
+ this.nodeIds.add(id);
241
+ return id;
242
+ }
243
+ }
@@ -0,0 +1,67 @@
1
+ import type { GraphNode, GraphEdge } from './types.js';
2
+
3
+ const NODE_COLORS: Record<string, string> = {
4
+ workspace: '#ffd700', folder: '#8bc34a', file: '#64b5f6',
5
+ module: '#ce93d8', class: '#ff7043', interface: '#4dd0e1',
6
+ enum: '#ffb74d', function: '#81c784', method: '#aed581',
7
+ variable: '#e0e0e0',
8
+ };
9
+
10
+ export function renderGraphSVG(nodes: GraphNode[], edges: GraphEdge[], width = 800, height = 600): string {
11
+ const maxNodes = 200;
12
+ const visNodes = nodes.slice(0, maxNodes);
13
+ const nodeIds = new Set(visNodes.map(n => n.id));
14
+ const visEdges = edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
15
+
16
+ // Simple circular layout
17
+ const cx = width / 2, cy = height / 2;
18
+ const radius = Math.min(cx, cy) - 60;
19
+ const positioned = visNodes.map((n, i) => {
20
+ const angle = (i / visNodes.length) * Math.PI * 2 - Math.PI / 2;
21
+ return {
22
+ ...n,
23
+ x: cx + radius * Math.cos(angle),
24
+ y: cy + radius * Math.sin(angle),
25
+ r: 4,
26
+ color: NODE_COLORS[n.type] || '#888',
27
+ };
28
+ });
29
+ const nodeMap = new Map(positioned.map(n => [n.id, n]));
30
+
31
+ let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" style="background:#0d1117">
32
+ <defs>
33
+ <filter id="glow"><feGaussianBlur stdDeviation="2" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
34
+ </defs>
35
+ <rect width="${width}" height="${height}" fill="#0d1117"/>
36
+ <text x="20" y="28" fill="#8b949e" font-size="13" font-family="sans-serif">EDHIndex Knowledge Graph — ${nodes.length} nodes, ${edges.length} edges</text>`;
37
+
38
+ for (const e of visEdges) {
39
+ const s = nodeMap.get(e.source);
40
+ const t = nodeMap.get(e.target);
41
+ if (!s || !t) continue;
42
+ const opacity = e.type === 'imports' ? 0.6 : e.type === 'contains' ? 0.2 : 0.35;
43
+ const color = e.type === 'imports' ? '#4dd0e1' : e.type === 'exports' ? '#ffb74d' : '#555';
44
+ svg += `<line x1="${s.x}" y1="${s.y}" x2="${t.x}" y2="${t.y}" stroke="${color}" stroke-width="1" stroke-opacity="${opacity}"/>`;
45
+ }
46
+
47
+ for (const n of positioned) {
48
+ svg += `<circle cx="${n.x}" cy="${n.y}" r="${n.r}" fill="${n.color}" opacity="0.9" filter="url(#glow)"><title>${escapeXml(n.label)} (${n.type})\n${n.file}</title></circle>`;
49
+ }
50
+
51
+ // Legend
52
+ const types = ['workspace', 'folder', 'file', 'module', 'class', 'interface', 'enum', 'function', 'method'];
53
+ let ly = height - 30 * types.length - 10;
54
+ for (const t of types) {
55
+ const c = NODE_COLORS[t];
56
+ if (!c) continue;
57
+ svg += `<circle cx="18" cy="${ly}" r="4" fill="${c}"/><text x="28" y="${ly + 4}" fill="#8b949e" font-size="10" font-family="sans-serif">${t.charAt(0).toUpperCase() + t.slice(1)}</text>`;
58
+ ly += 18;
59
+ }
60
+
61
+ svg += '</svg>';
62
+ return svg;
63
+ }
64
+
65
+ function escapeXml(s: string): string {
66
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
67
+ }