@optave/codegraph 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/db.js CHANGED
@@ -144,6 +144,87 @@ export const MIGRATIONS = [
144
144
  CREATE INDEX IF NOT EXISTS idx_fc_mi ON function_complexity(maintainability_index ASC);
145
145
  `,
146
146
  },
147
+ {
148
+ version: 10,
149
+ up: `
150
+ CREATE TABLE IF NOT EXISTS dataflow (
151
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
152
+ source_id INTEGER NOT NULL,
153
+ target_id INTEGER NOT NULL,
154
+ kind TEXT NOT NULL,
155
+ param_index INTEGER,
156
+ expression TEXT,
157
+ line INTEGER,
158
+ confidence REAL DEFAULT 1.0,
159
+ FOREIGN KEY(source_id) REFERENCES nodes(id),
160
+ FOREIGN KEY(target_id) REFERENCES nodes(id)
161
+ );
162
+ CREATE INDEX IF NOT EXISTS idx_dataflow_source ON dataflow(source_id);
163
+ CREATE INDEX IF NOT EXISTS idx_dataflow_target ON dataflow(target_id);
164
+ CREATE INDEX IF NOT EXISTS idx_dataflow_kind ON dataflow(kind);
165
+ CREATE INDEX IF NOT EXISTS idx_dataflow_source_kind ON dataflow(source_id, kind);
166
+ `,
167
+ },
168
+ {
169
+ version: 11,
170
+ up: `
171
+ ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id);
172
+ CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id);
173
+ CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id);
174
+ `,
175
+ },
176
+ {
177
+ version: 12,
178
+ up: `
179
+ CREATE TABLE IF NOT EXISTS cfg_blocks (
180
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
181
+ function_node_id INTEGER NOT NULL,
182
+ block_index INTEGER NOT NULL,
183
+ block_type TEXT NOT NULL,
184
+ start_line INTEGER,
185
+ end_line INTEGER,
186
+ label TEXT,
187
+ FOREIGN KEY(function_node_id) REFERENCES nodes(id),
188
+ UNIQUE(function_node_id, block_index)
189
+ );
190
+ CREATE INDEX IF NOT EXISTS idx_cfg_blocks_fn ON cfg_blocks(function_node_id);
191
+
192
+ CREATE TABLE IF NOT EXISTS cfg_edges (
193
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
194
+ function_node_id INTEGER NOT NULL,
195
+ source_block_id INTEGER NOT NULL,
196
+ target_block_id INTEGER NOT NULL,
197
+ kind TEXT NOT NULL,
198
+ FOREIGN KEY(function_node_id) REFERENCES nodes(id),
199
+ FOREIGN KEY(source_block_id) REFERENCES cfg_blocks(id),
200
+ FOREIGN KEY(target_block_id) REFERENCES cfg_blocks(id)
201
+ );
202
+ CREATE INDEX IF NOT EXISTS idx_cfg_edges_fn ON cfg_edges(function_node_id);
203
+ CREATE INDEX IF NOT EXISTS idx_cfg_edges_src ON cfg_edges(source_block_id);
204
+ CREATE INDEX IF NOT EXISTS idx_cfg_edges_tgt ON cfg_edges(target_block_id);
205
+ `,
206
+ },
207
+ {
208
+ version: 13,
209
+ up: `
210
+ CREATE TABLE IF NOT EXISTS ast_nodes (
211
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
212
+ file TEXT NOT NULL,
213
+ line INTEGER NOT NULL,
214
+ kind TEXT NOT NULL,
215
+ name TEXT NOT NULL,
216
+ text TEXT,
217
+ receiver TEXT,
218
+ parent_node_id INTEGER,
219
+ FOREIGN KEY(parent_node_id) REFERENCES nodes(id)
220
+ );
221
+ CREATE INDEX IF NOT EXISTS idx_ast_kind ON ast_nodes(kind);
222
+ CREATE INDEX IF NOT EXISTS idx_ast_name ON ast_nodes(name);
223
+ CREATE INDEX IF NOT EXISTS idx_ast_file ON ast_nodes(file);
224
+ CREATE INDEX IF NOT EXISTS idx_ast_parent ON ast_nodes(parent_node_id);
225
+ CREATE INDEX IF NOT EXISTS idx_ast_kind_name ON ast_nodes(kind, name);
226
+ `,
227
+ },
147
228
  ];
148
229
 
149
230
  export function getBuildMeta(db, key) {
@@ -265,6 +346,21 @@ export function initSchema(db) {
265
346
  } catch {
266
347
  /* already exists */
267
348
  }
349
+ try {
350
+ db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
351
+ } catch {
352
+ /* already exists */
353
+ }
354
+ try {
355
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
356
+ } catch {
357
+ /* already exists */
358
+ }
359
+ try {
360
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
361
+ } catch {
362
+ /* already exists */
363
+ }
268
364
  }
269
365
 
270
366
  export function findDbPath(customPath) {
package/src/embedder.js CHANGED
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { createInterface } from 'node:readline';
5
5
  import { closeDb, findDbPath, openDb, openReadonlyOrFail } from './db.js';
6
6
  import { info, warn } from './logger.js';
7
+ import { normalizeSymbol } from './queries.js';
7
8
 
8
9
  /**
9
10
  * Split an identifier into readable words.
@@ -582,7 +583,7 @@ function _prepareSearch(customDbPath, opts = {}) {
582
583
  const noTests = opts.noTests || false;
583
584
  const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
584
585
  let sql = `
585
- SELECT e.node_id, e.vector, e.text_preview, n.name, n.kind, n.file, n.line
586
+ SELECT e.node_id, e.vector, e.text_preview, n.name, n.kind, n.file, n.line, n.end_line, n.role
586
587
  FROM embeddings e
587
588
  JOIN nodes n ON e.node_id = n.id
588
589
  `;
@@ -638,6 +639,7 @@ export async function searchData(query, customDbPath, opts = {}) {
638
639
  return null;
639
640
  }
640
641
 
642
+ const hc = new Map();
641
643
  const results = [];
642
644
  for (const row of rows) {
643
645
  const vec = new Float32Array(new Uint8Array(row.vector).buffer);
@@ -645,10 +647,7 @@ export async function searchData(query, customDbPath, opts = {}) {
645
647
 
646
648
  if (sim >= minScore) {
647
649
  results.push({
648
- name: row.name,
649
- kind: row.kind,
650
- file: row.file,
651
- line: row.line,
650
+ ...normalizeSymbol(row, db, hc),
652
651
  similarity: sim,
653
652
  });
654
653
  }
@@ -734,14 +733,12 @@ export async function multiSearchData(queries, customDbPath, opts = {}) {
734
733
  }
735
734
 
736
735
  // Build results sorted by RRF score
736
+ const hc = new Map();
737
737
  const results = [];
738
738
  for (const [rowIndex, entry] of fusionMap) {
739
739
  const row = rows[rowIndex];
740
740
  results.push({
741
- name: row.name,
742
- kind: row.kind,
743
- file: row.file,
744
- line: row.line,
741
+ ...normalizeSymbol(row, db, hc),
745
742
  rrf: entry.rrfScore,
746
743
  queryScores: entry.queryScores,
747
744
  });
@@ -804,7 +801,7 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
804
801
 
805
802
  let sql = `
806
803
  SELECT f.rowid AS node_id, rank AS bm25_score,
807
- n.name, n.kind, n.file, n.line
804
+ n.name, n.kind, n.file, n.line, n.end_line, n.role
808
805
  FROM fts_index f
809
806
  JOIN nodes n ON f.rowid = n.id
810
807
  WHERE fts_index MATCH ?
@@ -841,16 +838,13 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
841
838
  rows = rows.filter((row) => !TEST_PATTERN.test(row.file));
842
839
  }
843
840
 
844
- db.close();
845
-
841
+ const hc = new Map();
846
842
  const results = rows.slice(0, limit).map((row) => ({
847
- name: row.name,
848
- kind: row.kind,
849
- file: row.file,
850
- line: row.line,
843
+ ...normalizeSymbol(row, db, hc),
851
844
  bm25Score: -row.bm25_score, // FTS5 rank is negative; negate for display
852
845
  }));
853
846
 
847
+ db.close();
854
848
  return { results };
855
849
  }
856
850
 
@@ -924,6 +918,9 @@ export async function hybridSearchData(query, customDbPath, opts = {}) {
924
918
  kind: item.kind,
925
919
  file: item.file,
926
920
  line: item.line,
921
+ endLine: item.endLine ?? null,
922
+ role: item.role ?? null,
923
+ fileHash: item.fileHash ?? null,
927
924
  rrfScore: 0,
928
925
  bm25Score: null,
929
926
  bm25Rank: null,
@@ -955,6 +952,9 @@ export async function hybridSearchData(query, customDbPath, opts = {}) {
955
952
  kind: e.kind,
956
953
  file: e.file,
957
954
  line: e.line,
955
+ endLine: e.endLine,
956
+ role: e.role,
957
+ fileHash: e.fileHash,
958
958
  rrf: e.rrfScore,
959
959
  bm25Score: e.bm25Score,
960
960
  bm25Rank: e.bm25Rank,
package/src/export.js CHANGED
@@ -4,6 +4,25 @@ import { isTestFile } from './queries.js';
4
4
 
5
5
  const DEFAULT_MIN_CONFIDENCE = 0.5;
6
6
 
7
+ /** Escape special XML characters. */
8
+ function escapeXml(s) {
9
+ return String(s)
10
+ .replace(/&/g, '&')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&apos;');
15
+ }
16
+
17
+ /** RFC 4180 CSV field escaping — quote fields containing commas, quotes, or newlines. */
18
+ function escapeCsv(s) {
19
+ const str = String(s);
20
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
21
+ return `"${str.replace(/"/g, '""')}"`;
22
+ }
23
+ return str;
24
+ }
25
+
7
26
  /**
8
27
  * Export the dependency graph in DOT (Graphviz) format.
9
28
  */
@@ -374,3 +393,289 @@ export function exportJSON(db, opts = {}) {
374
393
  const base = { nodes, edges };
375
394
  return paginateResult(base, 'edges', { limit: opts.limit, offset: opts.offset });
376
395
  }
396
+
397
+ /**
398
+ * Export the dependency graph in GraphML (XML) format.
399
+ */
400
+ export function exportGraphML(db, opts = {}) {
401
+ const fileLevel = opts.fileLevel !== false;
402
+ const noTests = opts.noTests || false;
403
+ const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
404
+ const edgeLimit = opts.limit;
405
+
406
+ const lines = [
407
+ '<?xml version="1.0" encoding="UTF-8"?>',
408
+ '<graphml xmlns="http://graphml.graphstruct.net/graphml">',
409
+ ];
410
+
411
+ if (fileLevel) {
412
+ lines.push(' <key id="d0" for="node" attr.name="name" attr.type="string"/>');
413
+ lines.push(' <key id="d1" for="node" attr.name="file" attr.type="string"/>');
414
+ lines.push(' <key id="d2" for="edge" attr.name="kind" attr.type="string"/>');
415
+ lines.push(' <graph id="codegraph" edgedefault="directed">');
416
+
417
+ let edges = db
418
+ .prepare(`
419
+ SELECT DISTINCT n1.file AS source, n2.file AS target
420
+ FROM edges e
421
+ JOIN nodes n1 ON e.source_id = n1.id
422
+ JOIN nodes n2 ON e.target_id = n2.id
423
+ WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
424
+ AND e.confidence >= ?
425
+ `)
426
+ .all(minConf);
427
+ if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
428
+ if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
429
+
430
+ const files = new Set();
431
+ for (const { source, target } of edges) {
432
+ files.add(source);
433
+ files.add(target);
434
+ }
435
+
436
+ const fileIds = new Map();
437
+ let nIdx = 0;
438
+ for (const f of files) {
439
+ const id = `n${nIdx++}`;
440
+ fileIds.set(f, id);
441
+ lines.push(` <node id="${id}">`);
442
+ lines.push(` <data key="d0">${escapeXml(path.basename(f))}</data>`);
443
+ lines.push(` <data key="d1">${escapeXml(f)}</data>`);
444
+ lines.push(' </node>');
445
+ }
446
+
447
+ let eIdx = 0;
448
+ for (const { source, target } of edges) {
449
+ lines.push(
450
+ ` <edge id="e${eIdx++}" source="${fileIds.get(source)}" target="${fileIds.get(target)}">`,
451
+ );
452
+ lines.push(' <data key="d2">imports</data>');
453
+ lines.push(' </edge>');
454
+ }
455
+ } else {
456
+ lines.push(' <key id="d0" for="node" attr.name="name" attr.type="string"/>');
457
+ lines.push(' <key id="d1" for="node" attr.name="kind" attr.type="string"/>');
458
+ lines.push(' <key id="d2" for="node" attr.name="file" attr.type="string"/>');
459
+ lines.push(' <key id="d3" for="node" attr.name="line" attr.type="int"/>');
460
+ lines.push(' <key id="d4" for="node" attr.name="role" attr.type="string"/>');
461
+ lines.push(' <key id="d5" for="edge" attr.name="kind" attr.type="string"/>');
462
+ lines.push(' <key id="d6" for="edge" attr.name="confidence" attr.type="double"/>');
463
+ lines.push(' <graph id="codegraph" edgedefault="directed">');
464
+
465
+ let edges = db
466
+ .prepare(`
467
+ SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
468
+ n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
469
+ n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
470
+ n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
471
+ e.kind AS edge_kind, e.confidence
472
+ FROM edges e
473
+ JOIN nodes n1 ON e.source_id = n1.id
474
+ JOIN nodes n2 ON e.target_id = n2.id
475
+ WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
476
+ AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
477
+ AND e.kind = 'calls'
478
+ AND e.confidence >= ?
479
+ `)
480
+ .all(minConf);
481
+ if (noTests)
482
+ edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
483
+ if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
484
+
485
+ const emittedNodes = new Set();
486
+ function emitNode(id, name, kind, file, line, role) {
487
+ if (emittedNodes.has(id)) return;
488
+ emittedNodes.add(id);
489
+ lines.push(` <node id="n${id}">`);
490
+ lines.push(` <data key="d0">${escapeXml(name)}</data>`);
491
+ lines.push(` <data key="d1">${escapeXml(kind)}</data>`);
492
+ lines.push(` <data key="d2">${escapeXml(file)}</data>`);
493
+ lines.push(` <data key="d3">${line}</data>`);
494
+ if (role) lines.push(` <data key="d4">${escapeXml(role)}</data>`);
495
+ lines.push(' </node>');
496
+ }
497
+
498
+ let eIdx = 0;
499
+ for (const e of edges) {
500
+ emitNode(
501
+ e.source_id,
502
+ e.source_name,
503
+ e.source_kind,
504
+ e.source_file,
505
+ e.source_line,
506
+ e.source_role,
507
+ );
508
+ emitNode(
509
+ e.target_id,
510
+ e.target_name,
511
+ e.target_kind,
512
+ e.target_file,
513
+ e.target_line,
514
+ e.target_role,
515
+ );
516
+ lines.push(` <edge id="e${eIdx++}" source="n${e.source_id}" target="n${e.target_id}">`);
517
+ lines.push(` <data key="d5">${escapeXml(e.edge_kind)}</data>`);
518
+ lines.push(` <data key="d6">${e.confidence}</data>`);
519
+ lines.push(' </edge>');
520
+ }
521
+ }
522
+
523
+ lines.push(' </graph>');
524
+ lines.push('</graphml>');
525
+ return lines.join('\n');
526
+ }
527
+
528
+ /**
529
+ * Export the dependency graph in TinkerPop GraphSON v3 format.
530
+ */
531
+ export function exportGraphSON(db, opts = {}) {
532
+ const noTests = opts.noTests || false;
533
+ const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
534
+
535
+ let nodes = db
536
+ .prepare(`
537
+ SELECT id, name, kind, file, line, role FROM nodes
538
+ WHERE kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'file')
539
+ `)
540
+ .all();
541
+ if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
542
+
543
+ let edges = db
544
+ .prepare(`
545
+ SELECT e.rowid AS id, n1.id AS outV, n2.id AS inV, e.kind, e.confidence
546
+ FROM edges e
547
+ JOIN nodes n1 ON e.source_id = n1.id
548
+ JOIN nodes n2 ON e.target_id = n2.id
549
+ WHERE e.confidence >= ?
550
+ `)
551
+ .all(minConf);
552
+ if (noTests) {
553
+ const nodeIds = new Set(nodes.map((n) => n.id));
554
+ edges = edges.filter((e) => nodeIds.has(e.outV) && nodeIds.has(e.inV));
555
+ }
556
+
557
+ const vertices = nodes.map((n) => ({
558
+ id: n.id,
559
+ label: n.kind,
560
+ properties: {
561
+ name: [{ id: 0, value: n.name }],
562
+ file: [{ id: 0, value: n.file }],
563
+ ...(n.line != null ? { line: [{ id: 0, value: n.line }] } : {}),
564
+ ...(n.role ? { role: [{ id: 0, value: n.role }] } : {}),
565
+ },
566
+ }));
567
+
568
+ const gEdges = edges.map((e) => ({
569
+ id: e.id,
570
+ label: e.kind,
571
+ inV: e.inV,
572
+ outV: e.outV,
573
+ properties: {
574
+ confidence: e.confidence,
575
+ },
576
+ }));
577
+
578
+ const base = { vertices, edges: gEdges };
579
+ return paginateResult(base, 'edges', { limit: opts.limit, offset: opts.offset });
580
+ }
581
+
582
+ /**
583
+ * Export the dependency graph as Neo4j bulk-import CSV files.
584
+ * Returns { nodes: string, relationships: string }.
585
+ */
586
+ export function exportNeo4jCSV(db, opts = {}) {
587
+ const fileLevel = opts.fileLevel !== false;
588
+ const noTests = opts.noTests || false;
589
+ const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
590
+ const edgeLimit = opts.limit;
591
+
592
+ if (fileLevel) {
593
+ let edges = db
594
+ .prepare(`
595
+ SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind, e.confidence
596
+ FROM edges e
597
+ JOIN nodes n1 ON e.source_id = n1.id
598
+ JOIN nodes n2 ON e.target_id = n2.id
599
+ WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type', 'calls')
600
+ AND e.confidence >= ?
601
+ `)
602
+ .all(minConf);
603
+ if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
604
+ if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
605
+
606
+ const files = new Map();
607
+ let idx = 0;
608
+ for (const { source, target } of edges) {
609
+ if (!files.has(source)) files.set(source, idx++);
610
+ if (!files.has(target)) files.set(target, idx++);
611
+ }
612
+
613
+ const nodeLines = ['nodeId:ID,name,file:string,:LABEL'];
614
+ for (const [file, id] of files) {
615
+ nodeLines.push(`${id},${escapeCsv(path.basename(file))},${escapeCsv(file)},File`);
616
+ }
617
+
618
+ const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
619
+ for (const e of edges) {
620
+ const edgeType = e.kind.toUpperCase().replace(/-/g, '_');
621
+ relLines.push(`${files.get(e.source)},${files.get(e.target)},${edgeType},${e.confidence}`);
622
+ }
623
+
624
+ return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
625
+ }
626
+
627
+ let edges = db
628
+ .prepare(`
629
+ SELECT n1.id AS source_id, n1.name AS source_name, n1.kind AS source_kind,
630
+ n1.file AS source_file, n1.line AS source_line, n1.role AS source_role,
631
+ n2.id AS target_id, n2.name AS target_name, n2.kind AS target_kind,
632
+ n2.file AS target_file, n2.line AS target_line, n2.role AS target_role,
633
+ e.kind AS edge_kind, e.confidence
634
+ FROM edges e
635
+ JOIN nodes n1 ON e.source_id = n1.id
636
+ JOIN nodes n2 ON e.target_id = n2.id
637
+ WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
638
+ AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
639
+ AND e.kind = 'calls'
640
+ AND e.confidence >= ?
641
+ `)
642
+ .all(minConf);
643
+ if (noTests)
644
+ edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
645
+ if (edgeLimit && edges.length > edgeLimit) edges = edges.slice(0, edgeLimit);
646
+
647
+ const emitted = new Set();
648
+ const nodeLines = ['nodeId:ID,name,kind,file:string,line:int,role,:LABEL'];
649
+ function emitNode(id, name, kind, file, line, role) {
650
+ if (emitted.has(id)) return;
651
+ emitted.add(id);
652
+ const label = kind.charAt(0).toUpperCase() + kind.slice(1);
653
+ nodeLines.push(
654
+ `${id},${escapeCsv(name)},${escapeCsv(kind)},${escapeCsv(file)},${line},${escapeCsv(role || '')},${label}`,
655
+ );
656
+ }
657
+
658
+ const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
659
+ for (const e of edges) {
660
+ emitNode(
661
+ e.source_id,
662
+ e.source_name,
663
+ e.source_kind,
664
+ e.source_file,
665
+ e.source_line,
666
+ e.source_role,
667
+ );
668
+ emitNode(
669
+ e.target_id,
670
+ e.target_name,
671
+ e.target_kind,
672
+ e.target_file,
673
+ e.target_line,
674
+ e.target_role,
675
+ );
676
+ const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
677
+ relLines.push(`${e.source_id},${e.target_id},${edgeType},${e.confidence}`);
678
+ }
679
+
680
+ return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
681
+ }
@@ -33,11 +33,13 @@ export function extractCSharpSymbols(tree, _filePath) {
33
33
  case 'class_declaration': {
34
34
  const nameNode = node.childForFieldName('name');
35
35
  if (nameNode) {
36
+ const classChildren = extractCSharpClassFields(node);
36
37
  definitions.push({
37
38
  name: nameNode.text,
38
39
  kind: 'class',
39
40
  line: node.startPosition.row + 1,
40
41
  endLine: nodeEndLine(node),
42
+ children: classChildren.length > 0 ? classChildren : undefined,
41
43
  });
42
44
  extractCSharpBaseTypes(node, nameNode.text, classes);
43
45
  }
@@ -47,11 +49,13 @@ export function extractCSharpSymbols(tree, _filePath) {
47
49
  case 'struct_declaration': {
48
50
  const nameNode = node.childForFieldName('name');
49
51
  if (nameNode) {
52
+ const structChildren = extractCSharpClassFields(node);
50
53
  definitions.push({
51
54
  name: nameNode.text,
52
55
  kind: 'struct',
53
56
  line: node.startPosition.row + 1,
54
57
  endLine: nodeEndLine(node),
58
+ children: structChildren.length > 0 ? structChildren : undefined,
55
59
  });
56
60
  extractCSharpBaseTypes(node, nameNode.text, classes);
57
61
  }
@@ -105,11 +109,13 @@ export function extractCSharpSymbols(tree, _filePath) {
105
109
  case 'enum_declaration': {
106
110
  const nameNode = node.childForFieldName('name');
107
111
  if (nameNode) {
112
+ const enumChildren = extractCSharpEnumMembers(node);
108
113
  definitions.push({
109
114
  name: nameNode.text,
110
115
  kind: 'enum',
111
116
  line: node.startPosition.row + 1,
112
117
  endLine: nodeEndLine(node),
118
+ children: enumChildren.length > 0 ? enumChildren : undefined,
113
119
  });
114
120
  }
115
121
  break;
@@ -120,11 +126,13 @@ export function extractCSharpSymbols(tree, _filePath) {
120
126
  if (nameNode) {
121
127
  const parentType = findCSharpParentType(node);
122
128
  const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
129
+ const params = extractCSharpParameters(node.childForFieldName('parameters'));
123
130
  definitions.push({
124
131
  name: fullName,
125
132
  kind: 'method',
126
133
  line: node.startPosition.row + 1,
127
134
  endLine: nodeEndLine(node),
135
+ children: params.length > 0 ? params : undefined,
128
136
  });
129
137
  }
130
138
  break;
@@ -135,11 +143,13 @@ export function extractCSharpSymbols(tree, _filePath) {
135
143
  if (nameNode) {
136
144
  const parentType = findCSharpParentType(node);
137
145
  const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
146
+ const params = extractCSharpParameters(node.childForFieldName('parameters'));
138
147
  definitions.push({
139
148
  name: fullName,
140
149
  kind: 'method',
141
150
  line: node.startPosition.row + 1,
142
151
  endLine: nodeEndLine(node),
152
+ children: params.length > 0 ? params : undefined,
143
153
  });
144
154
  }
145
155
  break;
@@ -152,7 +162,7 @@ export function extractCSharpSymbols(tree, _filePath) {
152
162
  const fullName = parentType ? `${parentType}.${nameNode.text}` : nameNode.text;
153
163
  definitions.push({
154
164
  name: fullName,
155
- kind: 'method',
165
+ kind: 'property',
156
166
  line: node.startPosition.row + 1,
157
167
  endLine: nodeEndLine(node),
158
168
  });
@@ -220,6 +230,59 @@ export function extractCSharpSymbols(tree, _filePath) {
220
230
  return { definitions, calls, imports, classes, exports };
221
231
  }
222
232
 
233
+ // ── Child extraction helpers ────────────────────────────────────────────────
234
+
235
+ function extractCSharpParameters(paramListNode) {
236
+ const params = [];
237
+ if (!paramListNode) return params;
238
+ for (let i = 0; i < paramListNode.childCount; i++) {
239
+ const param = paramListNode.child(i);
240
+ if (!param || param.type !== 'parameter') continue;
241
+ const nameNode = param.childForFieldName('name');
242
+ if (nameNode) {
243
+ params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
244
+ }
245
+ }
246
+ return params;
247
+ }
248
+
249
+ function extractCSharpClassFields(classNode) {
250
+ const fields = [];
251
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'declaration_list');
252
+ if (!body) return fields;
253
+ for (let i = 0; i < body.childCount; i++) {
254
+ const member = body.child(i);
255
+ if (!member || member.type !== 'field_declaration') continue;
256
+ const varDecl = findChild(member, 'variable_declaration');
257
+ if (!varDecl) continue;
258
+ for (let j = 0; j < varDecl.childCount; j++) {
259
+ const child = varDecl.child(j);
260
+ if (!child || child.type !== 'variable_declarator') continue;
261
+ const nameNode = child.childForFieldName('name');
262
+ if (nameNode) {
263
+ fields.push({ name: nameNode.text, kind: 'property', line: member.startPosition.row + 1 });
264
+ }
265
+ }
266
+ }
267
+ return fields;
268
+ }
269
+
270
+ function extractCSharpEnumMembers(enumNode) {
271
+ const constants = [];
272
+ const body =
273
+ enumNode.childForFieldName('body') || findChild(enumNode, 'enum_member_declaration_list');
274
+ if (!body) return constants;
275
+ for (let i = 0; i < body.childCount; i++) {
276
+ const member = body.child(i);
277
+ if (!member || member.type !== 'enum_member_declaration') continue;
278
+ const nameNode = member.childForFieldName('name');
279
+ if (nameNode) {
280
+ constants.push({ name: nameNode.text, kind: 'constant', line: member.startPosition.row + 1 });
281
+ }
282
+ }
283
+ return constants;
284
+ }
285
+
223
286
  function extractCSharpBaseTypes(node, className, classes) {
224
287
  const baseList = node.childForFieldName('bases');
225
288
  if (!baseList) return;