@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/paginate.js CHANGED
@@ -9,17 +9,16 @@
9
9
  export const MCP_DEFAULTS = {
10
10
  // Existing
11
11
  list_functions: 100,
12
- query_function: 50,
12
+ query: 10,
13
13
  where: 50,
14
14
  node_roles: 100,
15
- list_entry_points: 100,
16
15
  export_graph: 500,
17
16
  // Smaller defaults for rich/nested results
18
- fn_deps: 10,
19
17
  fn_impact: 5,
20
18
  context: 5,
21
19
  explain: 10,
22
20
  file_deps: 20,
21
+ file_exports: 20,
23
22
  diff_impact: 30,
24
23
  impact_analysis: 20,
25
24
  semantic_search: 20,
@@ -31,6 +30,7 @@ export const MCP_DEFAULTS = {
31
30
  communities: 20,
32
31
  structure: 30,
33
32
  triage: 20,
33
+ ast_query: 50,
34
34
  };
35
35
 
36
36
  /** Hard cap to prevent abuse via MCP. */
package/src/parser.js CHANGED
@@ -142,6 +142,14 @@ function normalizeNativeSymbols(result) {
142
142
  maintainabilityIndex: d.complexity.maintainabilityIndex ?? null,
143
143
  }
144
144
  : null,
145
+ children: d.children?.length
146
+ ? d.children.map((c) => ({
147
+ name: c.name,
148
+ kind: c.kind,
149
+ line: c.line,
150
+ endLine: c.endLine ?? c.end_line ?? null,
151
+ }))
152
+ : undefined,
145
153
  })),
146
154
  calls: (result.calls || []).map((c) => ({
147
155
  name: c.name,
package/src/queries.js CHANGED
@@ -59,7 +59,9 @@ export const FALSE_POSITIVE_NAMES = new Set([
59
59
  export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
60
60
 
61
61
  const FUNCTION_KINDS = ['function', 'method', 'class'];
62
- export const ALL_SYMBOL_KINDS = [
62
+
63
+ // Original 10 kinds — used as default query scope
64
+ export const CORE_SYMBOL_KINDS = [
63
65
  'function',
64
66
  'method',
65
67
  'class',
@@ -72,6 +74,39 @@ export const ALL_SYMBOL_KINDS = [
72
74
  'module',
73
75
  ];
74
76
 
77
+ // Sub-declaration kinds (Phase 1)
78
+ export const EXTENDED_SYMBOL_KINDS = [
79
+ 'parameter',
80
+ 'property',
81
+ 'constant',
82
+ // Phase 2 (reserved, not yet extracted):
83
+ // 'constructor', 'namespace', 'decorator', 'getter', 'setter',
84
+ ];
85
+
86
+ // Full set for --kind validation and MCP enum
87
+ export const EVERY_SYMBOL_KIND = [...CORE_SYMBOL_KINDS, ...EXTENDED_SYMBOL_KINDS];
88
+
89
+ // Backward compat: ALL_SYMBOL_KINDS stays as the core 10
90
+ export const ALL_SYMBOL_KINDS = CORE_SYMBOL_KINDS;
91
+
92
+ // ── Edge kind constants ─────────────────────────────────────────────
93
+ // Core edge kinds — coupling and dependency relationships
94
+ export const CORE_EDGE_KINDS = [
95
+ 'imports',
96
+ 'imports-type',
97
+ 'reexports',
98
+ 'calls',
99
+ 'extends',
100
+ 'implements',
101
+ 'contains',
102
+ ];
103
+
104
+ // Structural edge kinds — parent/child and type relationships
105
+ export const STRUCTURAL_EDGE_KINDS = ['parameter_of', 'receiver'];
106
+
107
+ // Full set for MCP enum and validation
108
+ export const EVERY_EDGE_KIND = [...CORE_EDGE_KINDS, ...STRUCTURAL_EDGE_KINDS];
109
+
75
110
  export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
76
111
 
77
112
  /**
@@ -190,6 +225,12 @@ export function kindIcon(kind) {
190
225
  return 'I';
191
226
  case 'type':
192
227
  return 'T';
228
+ case 'parameter':
229
+ return 'p';
230
+ case 'property':
231
+ return '.';
232
+ case 'constant':
233
+ return 'C';
193
234
  default:
194
235
  return '-';
195
236
  }
@@ -207,6 +248,7 @@ export function queryNameData(name, customDbPath, opts = {}) {
207
248
  return { query: name, results: [] };
208
249
  }
209
250
 
251
+ const hc = new Map();
210
252
  const results = nodes.map((node) => {
211
253
  let callees = db
212
254
  .prepare(`
@@ -230,10 +272,7 @@ export function queryNameData(name, customDbPath, opts = {}) {
230
272
  }
231
273
 
232
274
  return {
233
- name: node.name,
234
- kind: node.kind,
235
- file: node.file,
236
- line: node.line,
275
+ ...normalizeSymbol(node, db, hc),
237
276
  callees: callees.map((c) => ({
238
277
  name: c.name,
239
278
  kind: c.kind,
@@ -327,12 +366,12 @@ export function moduleMapData(customDbPath, limit = 20, opts = {}) {
327
366
  const nodes = db
328
367
  .prepare(`
329
368
  SELECT n.*,
330
- (SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind != 'contains') as out_edges,
331
- (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind != 'contains') as in_edges
369
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as out_edges,
370
+ (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) as in_edges
332
371
  FROM nodes n
333
372
  WHERE n.kind = 'file'
334
373
  ${testFilter}
335
- ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind != 'contains') DESC
374
+ ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver')) DESC
336
375
  LIMIT ?
337
376
  `)
338
377
  .all(limit);
@@ -402,6 +441,7 @@ export function fnDepsData(name, customDbPath, opts = {}) {
402
441
  const db = openReadonlyOrFail(customDbPath);
403
442
  const depth = opts.depth || 3;
404
443
  const noTests = opts.noTests || false;
444
+ const hc = new Map();
405
445
 
406
446
  const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
407
447
  if (nodes.length === 0) {
@@ -493,10 +533,7 @@ export function fnDepsData(name, customDbPath, opts = {}) {
493
533
  }
494
534
 
495
535
  return {
496
- name: node.name,
497
- kind: node.kind,
498
- file: node.file,
499
- line: node.line,
536
+ ...normalizeSymbol(node, db, hc),
500
537
  callees: filteredCallees.map((c) => ({
501
538
  name: c.name,
502
539
  kind: c.kind,
@@ -523,6 +560,7 @@ export function fnImpactData(name, customDbPath, opts = {}) {
523
560
  const db = openReadonlyOrFail(customDbPath);
524
561
  const maxDepth = opts.depth || 5;
525
562
  const noTests = opts.noTests || false;
563
+ const hc = new Map();
526
564
 
527
565
  const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
528
566
  if (nodes.length === 0) {
@@ -559,10 +597,7 @@ export function fnImpactData(name, customDbPath, opts = {}) {
559
597
  }
560
598
 
561
599
  return {
562
- name: node.name,
563
- kind: node.kind,
564
- file: node.file,
565
- line: node.line,
600
+ ...normalizeSymbol(node, db, hc),
566
601
  levels,
567
602
  totalDependents: visited.size - 1,
568
603
  };
@@ -1194,14 +1229,16 @@ export function listFunctionsData(customDbPath, opts = {}) {
1194
1229
 
1195
1230
  let rows = db
1196
1231
  .prepare(
1197
- `SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
1232
+ `SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
1198
1233
  )
1199
1234
  .all(...params);
1200
1235
 
1201
1236
  if (noTests) rows = rows.filter((r) => !isTestFile(r.file));
1202
1237
 
1238
+ const hc = new Map();
1239
+ const functions = rows.map((r) => normalizeSymbol(r, db, hc));
1203
1240
  db.close();
1204
- const base = { count: rows.length, functions: rows };
1241
+ const base = { count: functions.length, functions };
1205
1242
  return paginateResult(base, 'functions', { limit: opts.limit, offset: opts.offset });
1206
1243
  }
1207
1244
 
@@ -1234,11 +1271,18 @@ export function* iterListFunctions(customDbPath, opts = {}) {
1234
1271
  }
1235
1272
 
1236
1273
  const stmt = db.prepare(
1237
- `SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
1274
+ `SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY file, line`,
1238
1275
  );
1239
1276
  for (const row of stmt.iterate(...params)) {
1240
1277
  if (noTests && isTestFile(row.file)) continue;
1241
- yield { name: row.name, kind: row.kind, file: row.file, line: row.line, role: row.role };
1278
+ yield {
1279
+ name: row.name,
1280
+ kind: row.kind,
1281
+ file: row.file,
1282
+ line: row.line,
1283
+ endLine: row.end_line ?? null,
1284
+ role: row.role ?? null,
1285
+ };
1242
1286
  }
1243
1287
  } finally {
1244
1288
  db.close();
@@ -1252,7 +1296,7 @@ export function* iterListFunctions(customDbPath, opts = {}) {
1252
1296
  * @param {boolean} [opts.noTests]
1253
1297
  * @param {string} [opts.role]
1254
1298
  * @param {string} [opts.file]
1255
- * @yields {{ name: string, kind: string, file: string, line: number, role: string }}
1299
+ * @yields {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string }}
1256
1300
  */
1257
1301
  export function* iterRoles(customDbPath, opts = {}) {
1258
1302
  const db = openReadonlyOrFail(customDbPath);
@@ -1271,11 +1315,18 @@ export function* iterRoles(customDbPath, opts = {}) {
1271
1315
  }
1272
1316
 
1273
1317
  const stmt = db.prepare(
1274
- `SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
1318
+ `SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
1275
1319
  );
1276
1320
  for (const row of stmt.iterate(...params)) {
1277
1321
  if (noTests && isTestFile(row.file)) continue;
1278
- yield { name: row.name, kind: row.kind, file: row.file, line: row.line, role: row.role };
1322
+ yield {
1323
+ name: row.name,
1324
+ kind: row.kind,
1325
+ file: row.file,
1326
+ line: row.line,
1327
+ endLine: row.end_line ?? null,
1328
+ role: row.role ?? null,
1329
+ };
1279
1330
  }
1280
1331
  } finally {
1281
1332
  db.close();
@@ -2214,6 +2265,17 @@ export function contextData(name, customDbPath, opts = {}) {
2214
2265
  /* table may not exist */
2215
2266
  }
2216
2267
 
2268
+ // Children (parameters, properties, constants)
2269
+ let nodeChildren = [];
2270
+ try {
2271
+ nodeChildren = db
2272
+ .prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
2273
+ .all(node.id)
2274
+ .map((c) => ({ name: c.name, kind: c.kind, line: c.line, endLine: c.end_line || null }));
2275
+ } catch {
2276
+ /* parent_id column may not exist */
2277
+ }
2278
+
2217
2279
  return {
2218
2280
  name: node.name,
2219
2281
  kind: node.kind,
@@ -2224,6 +2286,7 @@ export function contextData(name, customDbPath, opts = {}) {
2224
2286
  source,
2225
2287
  signature,
2226
2288
  complexity: complexityMetrics,
2289
+ children: nodeChildren.length > 0 ? nodeChildren : undefined,
2227
2290
  callees,
2228
2291
  callers,
2229
2292
  relatedTests,
@@ -2263,6 +2326,15 @@ export function context(name, customDbPath, opts = {}) {
2263
2326
  console.log();
2264
2327
  }
2265
2328
 
2329
+ // Children
2330
+ if (r.children && r.children.length > 0) {
2331
+ console.log(`## Children (${r.children.length})`);
2332
+ for (const c of r.children) {
2333
+ console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
2334
+ }
2335
+ console.log();
2336
+ }
2337
+
2266
2338
  // Complexity
2267
2339
  if (r.complexity) {
2268
2340
  const cx = r.complexity;
@@ -2335,6 +2407,69 @@ export function context(name, customDbPath, opts = {}) {
2335
2407
  }
2336
2408
  }
2337
2409
 
2410
+ // ─── childrenData ───────────────────────────────────────────────────────
2411
+
2412
+ export function childrenData(name, customDbPath, opts = {}) {
2413
+ const db = openReadonlyOrFail(customDbPath);
2414
+ const noTests = opts.noTests || false;
2415
+
2416
+ const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
2417
+ if (nodes.length === 0) {
2418
+ db.close();
2419
+ return { name, results: [] };
2420
+ }
2421
+
2422
+ const results = nodes.map((node) => {
2423
+ let children;
2424
+ try {
2425
+ children = db
2426
+ .prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
2427
+ .all(node.id);
2428
+ } catch {
2429
+ children = [];
2430
+ }
2431
+ if (noTests) children = children.filter((c) => !isTestFile(c.file || node.file));
2432
+ return {
2433
+ name: node.name,
2434
+ kind: node.kind,
2435
+ file: node.file,
2436
+ line: node.line,
2437
+ children: children.map((c) => ({
2438
+ name: c.name,
2439
+ kind: c.kind,
2440
+ line: c.line,
2441
+ endLine: c.end_line || null,
2442
+ })),
2443
+ };
2444
+ });
2445
+
2446
+ db.close();
2447
+ const base = { name, results };
2448
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
2449
+ }
2450
+
2451
+ export function children(name, customDbPath, opts = {}) {
2452
+ const data = childrenData(name, customDbPath, opts);
2453
+ if (opts.json) {
2454
+ console.log(JSON.stringify(data, null, 2));
2455
+ return;
2456
+ }
2457
+ if (data.results.length === 0) {
2458
+ console.log(`No symbol matching "${name}"`);
2459
+ return;
2460
+ }
2461
+ for (const r of data.results) {
2462
+ console.log(`\n${kindIcon(r.kind)} ${r.name} ${r.file}:${r.line}`);
2463
+ if (r.children.length === 0) {
2464
+ console.log(' (no children)');
2465
+ } else {
2466
+ for (const c of r.children) {
2467
+ console.log(` ${kindIcon(c.kind)} ${c.name} :${c.line}`);
2468
+ }
2469
+ }
2470
+ }
2471
+ }
2472
+
2338
2473
  // ─── explainData ────────────────────────────────────────────────────────
2339
2474
 
2340
2475
  function isFileLikeTarget(target) {
@@ -2457,6 +2592,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
2457
2592
  if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
2458
2593
  if (nodes.length === 0) return [];
2459
2594
 
2595
+ const hc = new Map();
2460
2596
  return nodes.slice(0, 10).map((node) => {
2461
2597
  const fileLines = getFileLines(node.file);
2462
2598
  const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
@@ -2514,12 +2650,7 @@ function explainFunctionImpl(db, target, noTests, getFileLines) {
2514
2650
  }
2515
2651
 
2516
2652
  return {
2517
- name: node.name,
2518
- kind: node.kind,
2519
- file: node.file,
2520
- line: node.line,
2521
- role: node.role || null,
2522
- endLine: node.end_line || null,
2653
+ ...normalizeSymbol(node, db, hc),
2523
2654
  lineCount,
2524
2655
  summary,
2525
2656
  signature,
@@ -2732,6 +2863,41 @@ export function explain(target, customDbPath, opts = {}) {
2732
2863
 
2733
2864
  // ─── whereData ──────────────────────────────────────────────────────────
2734
2865
 
2866
+ function getFileHash(db, file) {
2867
+ const row = db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file);
2868
+ return row ? row.hash : null;
2869
+ }
2870
+
2871
+ /**
2872
+ * Normalize a raw DB/query row into the stable 7-field symbol shape.
2873
+ * @param {object} row - Raw row (from SELECT * or explicit columns)
2874
+ * @param {object} [db] - Open DB handle; when null, fileHash will be null
2875
+ * @param {Map} [hashCache] - Optional per-file cache to avoid repeated getFileHash calls
2876
+ * @returns {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string|null, fileHash: string|null }}
2877
+ */
2878
+ export function normalizeSymbol(row, db, hashCache) {
2879
+ let fileHash = null;
2880
+ if (db) {
2881
+ if (hashCache) {
2882
+ if (!hashCache.has(row.file)) {
2883
+ hashCache.set(row.file, getFileHash(db, row.file));
2884
+ }
2885
+ fileHash = hashCache.get(row.file);
2886
+ } else {
2887
+ fileHash = getFileHash(db, row.file);
2888
+ }
2889
+ }
2890
+ return {
2891
+ name: row.name,
2892
+ kind: row.kind,
2893
+ file: row.file,
2894
+ line: row.line,
2895
+ endLine: row.end_line ?? row.endLine ?? null,
2896
+ role: row.role ?? null,
2897
+ fileHash,
2898
+ };
2899
+ }
2900
+
2735
2901
  function whereSymbolImpl(db, target, noTests) {
2736
2902
  const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
2737
2903
  let nodes = db
@@ -2741,6 +2907,7 @@ function whereSymbolImpl(db, target, noTests) {
2741
2907
  .all(`%${target}%`, ...ALL_SYMBOL_KINDS);
2742
2908
  if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
2743
2909
 
2910
+ const hc = new Map();
2744
2911
  return nodes.map((node) => {
2745
2912
  const crossFileCallers = db
2746
2913
  .prepare(
@@ -2759,11 +2926,7 @@ function whereSymbolImpl(db, target, noTests) {
2759
2926
  if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
2760
2927
 
2761
2928
  return {
2762
- name: node.name,
2763
- kind: node.kind,
2764
- file: node.file,
2765
- line: node.line,
2766
- role: node.role || null,
2929
+ ...normalizeSymbol(node, db, hc),
2767
2930
  exported,
2768
2931
  uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })),
2769
2932
  };
@@ -2813,6 +2976,7 @@ function whereFileImpl(db, target) {
2813
2976
 
2814
2977
  return {
2815
2978
  file: fn.file,
2979
+ fileHash: getFileHash(db, fn.file),
2816
2980
  symbols: symbols.map((s) => ({ name: s.name, kind: s.kind, line: s.line })),
2817
2981
  imports,
2818
2982
  importedBy,
@@ -2908,7 +3072,7 @@ export function rolesData(customDbPath, opts = {}) {
2908
3072
 
2909
3073
  let rows = db
2910
3074
  .prepare(
2911
- `SELECT name, kind, file, line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
3075
+ `SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
2912
3076
  )
2913
3077
  .all(...params);
2914
3078
 
@@ -2919,8 +3083,10 @@ export function rolesData(customDbPath, opts = {}) {
2919
3083
  summary[r.role] = (summary[r.role] || 0) + 1;
2920
3084
  }
2921
3085
 
3086
+ const hc = new Map();
3087
+ const symbols = rows.map((r) => normalizeSymbol(r, db, hc));
2922
3088
  db.close();
2923
- const base = { count: rows.length, summary, symbols: rows };
3089
+ const base = { count: symbols.length, summary, symbols };
2924
3090
  return paginateResult(base, 'symbols', { limit: opts.limit, offset: opts.offset });
2925
3091
  }
2926
3092
 
@@ -2966,6 +3132,166 @@ export function roles(customDbPath, opts = {}) {
2966
3132
  }
2967
3133
  }
2968
3134
 
3135
+ // ─── exportsData ─────────────────────────────────────────────────────
3136
+
3137
+ function exportsFileImpl(db, target, noTests, getFileLines) {
3138
+ const fileNodes = db
3139
+ .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
3140
+ .all(`%${target}%`);
3141
+ if (fileNodes.length === 0) return [];
3142
+
3143
+ return fileNodes.map((fn) => {
3144
+ const symbols = db
3145
+ .prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
3146
+ .all(fn.file);
3147
+
3148
+ // IDs of symbols that have incoming calls from other files (exported)
3149
+ const exportedIds = new Set(
3150
+ db
3151
+ .prepare(
3152
+ `SELECT DISTINCT e.target_id FROM edges e
3153
+ JOIN nodes caller ON e.source_id = caller.id
3154
+ JOIN nodes target ON e.target_id = target.id
3155
+ WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
3156
+ )
3157
+ .all(fn.file, fn.file)
3158
+ .map((r) => r.target_id),
3159
+ );
3160
+
3161
+ const exported = symbols.filter((s) => exportedIds.has(s.id));
3162
+ const internalCount = symbols.length - exported.length;
3163
+
3164
+ const results = exported.map((s) => {
3165
+ const fileLines = getFileLines(fn.file);
3166
+
3167
+ let consumers = db
3168
+ .prepare(
3169
+ `SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
3170
+ WHERE e.target_id = ? AND e.kind = 'calls'`,
3171
+ )
3172
+ .all(s.id);
3173
+ if (noTests) consumers = consumers.filter((c) => !isTestFile(c.file));
3174
+
3175
+ return {
3176
+ name: s.name,
3177
+ kind: s.kind,
3178
+ line: s.line,
3179
+ endLine: s.end_line ?? null,
3180
+ role: s.role || null,
3181
+ signature: fileLines ? extractSignature(fileLines, s.line) : null,
3182
+ summary: fileLines ? extractSummary(fileLines, s.line) : null,
3183
+ consumers: consumers.map((c) => ({ name: c.name, file: c.file, line: c.line })),
3184
+ consumerCount: consumers.length,
3185
+ };
3186
+ });
3187
+
3188
+ // Files that re-export this file (barrel → this file)
3189
+ const reexports = db
3190
+ .prepare(
3191
+ `SELECT DISTINCT n.file FROM edges e JOIN nodes n ON e.source_id = n.id
3192
+ WHERE e.target_id = ? AND e.kind = 'reexports'`,
3193
+ )
3194
+ .all(fn.id)
3195
+ .map((r) => ({ file: r.file }));
3196
+
3197
+ return {
3198
+ file: fn.file,
3199
+ results,
3200
+ reexports,
3201
+ totalExported: exported.length,
3202
+ totalInternal: internalCount,
3203
+ };
3204
+ });
3205
+ }
3206
+
3207
+ export function exportsData(file, customDbPath, opts = {}) {
3208
+ const db = openReadonlyOrFail(customDbPath);
3209
+ const noTests = opts.noTests || false;
3210
+
3211
+ const dbFilePath = findDbPath(customDbPath);
3212
+ const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
3213
+
3214
+ const fileCache = new Map();
3215
+ function getFileLines(file) {
3216
+ if (fileCache.has(file)) return fileCache.get(file);
3217
+ try {
3218
+ const absPath = safePath(repoRoot, file);
3219
+ if (!absPath) {
3220
+ fileCache.set(file, null);
3221
+ return null;
3222
+ }
3223
+ const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
3224
+ fileCache.set(file, lines);
3225
+ return lines;
3226
+ } catch {
3227
+ fileCache.set(file, null);
3228
+ return null;
3229
+ }
3230
+ }
3231
+
3232
+ const fileResults = exportsFileImpl(db, file, noTests, getFileLines);
3233
+ db.close();
3234
+
3235
+ if (fileResults.length === 0) {
3236
+ return paginateResult(
3237
+ { file, results: [], reexports: [], totalExported: 0, totalInternal: 0 },
3238
+ 'results',
3239
+ { limit: opts.limit, offset: opts.offset },
3240
+ );
3241
+ }
3242
+
3243
+ // For single-file match return flat; for multi-match return first (like explainData)
3244
+ const first = fileResults[0];
3245
+ const base = {
3246
+ file: first.file,
3247
+ results: first.results,
3248
+ reexports: first.reexports,
3249
+ totalExported: first.totalExported,
3250
+ totalInternal: first.totalInternal,
3251
+ };
3252
+ return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
3253
+ }
3254
+
3255
+ export function fileExports(file, customDbPath, opts = {}) {
3256
+ const data = exportsData(file, customDbPath, opts);
3257
+ if (opts.ndjson) {
3258
+ printNdjson(data, 'results');
3259
+ return;
3260
+ }
3261
+ if (opts.json) {
3262
+ console.log(JSON.stringify(data, null, 2));
3263
+ return;
3264
+ }
3265
+
3266
+ if (data.results.length === 0) {
3267
+ console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
3268
+ return;
3269
+ }
3270
+
3271
+ console.log(
3272
+ `\n# ${data.file} — ${data.totalExported} exported, ${data.totalInternal} internal\n`,
3273
+ );
3274
+
3275
+ for (const sym of data.results) {
3276
+ const icon = kindIcon(sym.kind);
3277
+ const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
3278
+ const role = sym.role ? ` [${sym.role}]` : '';
3279
+ console.log(` ${icon} ${sym.name}${sig}${role} :${sym.line}`);
3280
+ if (sym.consumers.length === 0) {
3281
+ console.log(' (no consumers)');
3282
+ } else {
3283
+ for (const c of sym.consumers) {
3284
+ console.log(` <- ${c.name} (${c.file}:${c.line})`);
3285
+ }
3286
+ }
3287
+ }
3288
+
3289
+ if (data.reexports.length > 0) {
3290
+ console.log(`\n Re-exports: ${data.reexports.map((r) => r.file).join(', ')}`);
3291
+ }
3292
+ console.log();
3293
+ }
3294
+
2969
3295
  export function fnImpact(name, customDbPath, opts = {}) {
2970
3296
  const data = fnImpactData(name, customDbPath, opts);
2971
3297
  if (opts.ndjson) {
package/src/structure.js CHANGED
@@ -34,8 +34,11 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
34
34
  `);
35
35
 
36
36
  // Clean previous directory nodes/edges (idempotent rebuild)
37
+ // Scope contains-edge delete to directory-sourced edges only,
38
+ // preserving symbol-level contains edges (file→def, class→method, etc.)
37
39
  db.exec(`
38
- DELETE FROM edges WHERE kind = 'contains';
40
+ DELETE FROM edges WHERE kind = 'contains'
41
+ AND source_id IN (SELECT id FROM nodes WHERE kind = 'directory');
39
42
  DELETE FROM node_metrics;
40
43
  DELETE FROM nodes WHERE kind = 'directory';
41
44
  `);