@optave/codegraph 3.1.1 → 3.1.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 (68) hide show
  1. package/package.json +7 -7
  2. package/src/ast-analysis/engine.js +365 -0
  3. package/src/ast-analysis/metrics.js +118 -0
  4. package/src/ast-analysis/visitor-utils.js +176 -0
  5. package/src/ast-analysis/visitor.js +162 -0
  6. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  7. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  8. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  9. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  10. package/src/ast.js +13 -140
  11. package/src/audit.js +2 -87
  12. package/src/batch.js +0 -25
  13. package/src/boundaries.js +1 -1
  14. package/src/branch-compare.js +1 -96
  15. package/src/builder.js +48 -179
  16. package/src/cfg.js +89 -883
  17. package/src/check.js +1 -84
  18. package/src/cli.js +20 -19
  19. package/src/cochange.js +1 -39
  20. package/src/commands/audit.js +88 -0
  21. package/src/commands/batch.js +26 -0
  22. package/src/commands/branch-compare.js +97 -0
  23. package/src/commands/cfg.js +55 -0
  24. package/src/commands/check.js +82 -0
  25. package/src/commands/cochange.js +37 -0
  26. package/src/commands/communities.js +69 -0
  27. package/src/commands/complexity.js +77 -0
  28. package/src/commands/dataflow.js +110 -0
  29. package/src/commands/flow.js +70 -0
  30. package/src/commands/manifesto.js +77 -0
  31. package/src/commands/owners.js +52 -0
  32. package/src/commands/query.js +21 -0
  33. package/src/commands/sequence.js +33 -0
  34. package/src/commands/structure.js +64 -0
  35. package/src/commands/triage.js +49 -0
  36. package/src/communities.js +12 -83
  37. package/src/complexity.js +42 -356
  38. package/src/cycles.js +1 -1
  39. package/src/dataflow.js +12 -665
  40. package/src/db/repository/build-stmts.js +104 -0
  41. package/src/db/repository/cfg.js +83 -0
  42. package/src/db/repository/cochange.js +41 -0
  43. package/src/db/repository/complexity.js +15 -0
  44. package/src/db/repository/dataflow.js +12 -0
  45. package/src/db/repository/edges.js +259 -0
  46. package/src/db/repository/embeddings.js +40 -0
  47. package/src/db/repository/graph-read.js +39 -0
  48. package/src/db/repository/index.js +42 -0
  49. package/src/db/repository/nodes.js +236 -0
  50. package/src/db.js +40 -1
  51. package/src/embedder.js +14 -34
  52. package/src/export.js +1 -1
  53. package/src/extractors/javascript.js +130 -5
  54. package/src/flow.js +2 -70
  55. package/src/index.js +23 -19
  56. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  57. package/src/kinds.js +1 -0
  58. package/src/manifesto.js +0 -76
  59. package/src/owners.js +1 -56
  60. package/src/queries-cli.js +1 -1
  61. package/src/queries.js +79 -280
  62. package/src/sequence.js +5 -44
  63. package/src/structure.js +16 -75
  64. package/src/triage.js +1 -54
  65. package/src/viewer.js +1 -1
  66. package/src/watcher.js +7 -4
  67. package/src/db/repository.js +0 -134
  68. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
@@ -0,0 +1,236 @@
1
+ import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
2
+ import { NodeQuery } from '../query-builder.js';
3
+
4
+ // ─── Query-builder based lookups (moved from src/db/repository.js) ─────
5
+
6
+ /**
7
+ * Find nodes matching a name pattern, with fan-in count.
8
+ * @param {object} db
9
+ * @param {string} namePattern - LIKE pattern (already wrapped with %)
10
+ * @param {object} [opts]
11
+ * @param {string[]} [opts.kinds]
12
+ * @param {string} [opts.file]
13
+ * @returns {object[]}
14
+ */
15
+ export function findNodesWithFanIn(db, namePattern, opts = {}) {
16
+ const q = new NodeQuery()
17
+ .select('n.*, COALESCE(fi.cnt, 0) AS fan_in')
18
+ .withFanIn()
19
+ .where('n.name LIKE ?', namePattern);
20
+
21
+ if (opts.kinds) {
22
+ q.kinds(opts.kinds);
23
+ }
24
+ if (opts.file) {
25
+ q.fileFilter(opts.file);
26
+ }
27
+
28
+ return q.all(db);
29
+ }
30
+
31
+ /**
32
+ * Fetch nodes for triage scoring: fan-in + complexity + churn.
33
+ * @param {object} db
34
+ * @param {object} [opts]
35
+ * @returns {object[]}
36
+ */
37
+ export function findNodesForTriage(db, opts = {}) {
38
+ if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
39
+ throw new Error(`Invalid kind: ${opts.kind} (expected one of ${EVERY_SYMBOL_KIND.join(', ')})`);
40
+ }
41
+ if (opts.role && !VALID_ROLES.includes(opts.role)) {
42
+ throw new Error(`Invalid role: ${opts.role} (expected one of ${VALID_ROLES.join(', ')})`);
43
+ }
44
+
45
+ const kindsToUse = opts.kind ? [opts.kind] : ['function', 'method', 'class'];
46
+ const q = new NodeQuery()
47
+ .select(
48
+ `n.id, n.name, n.kind, n.file, n.line, n.end_line, n.role,
49
+ COALESCE(fi.cnt, 0) AS fan_in,
50
+ COALESCE(fc.cognitive, 0) AS cognitive,
51
+ COALESCE(fc.maintainability_index, 0) AS mi,
52
+ COALESCE(fc.cyclomatic, 0) AS cyclomatic,
53
+ COALESCE(fc.max_nesting, 0) AS max_nesting,
54
+ COALESCE(fcc.commit_count, 0) AS churn`,
55
+ )
56
+ .kinds(kindsToUse)
57
+ .withFanIn()
58
+ .withComplexity()
59
+ .withChurn()
60
+ .excludeTests(opts.noTests)
61
+ .fileFilter(opts.file)
62
+ .roleFilter(opts.role)
63
+ .orderBy('n.file, n.line');
64
+
65
+ return q.all(db);
66
+ }
67
+
68
+ /**
69
+ * Shared query builder for function/method/class node listing.
70
+ * @param {object} [opts]
71
+ * @returns {NodeQuery}
72
+ */
73
+ function _functionNodeQuery(opts = {}) {
74
+ return new NodeQuery()
75
+ .select('name, kind, file, line, end_line, role')
76
+ .kinds(['function', 'method', 'class'])
77
+ .fileFilter(opts.file)
78
+ .nameLike(opts.pattern)
79
+ .excludeTests(opts.noTests)
80
+ .orderBy('file, line');
81
+ }
82
+
83
+ /**
84
+ * List function/method/class nodes with basic info.
85
+ * @param {object} db
86
+ * @param {object} [opts]
87
+ * @returns {object[]}
88
+ */
89
+ export function listFunctionNodes(db, opts = {}) {
90
+ return _functionNodeQuery(opts).all(db);
91
+ }
92
+
93
+ /**
94
+ * Iterator version of listFunctionNodes for memory efficiency.
95
+ * @param {object} db
96
+ * @param {object} [opts]
97
+ * @returns {IterableIterator}
98
+ */
99
+ export function iterateFunctionNodes(db, opts = {}) {
100
+ return _functionNodeQuery(opts).iterate(db);
101
+ }
102
+
103
+ /**
104
+ * Count total nodes.
105
+ * @param {object} db
106
+ * @returns {number}
107
+ */
108
+ export function countNodes(db) {
109
+ return db.prepare('SELECT COUNT(*) AS cnt FROM nodes').get().cnt;
110
+ }
111
+
112
+ /**
113
+ * Count total edges.
114
+ * @param {object} db
115
+ * @returns {number}
116
+ */
117
+ export function countEdges(db) {
118
+ return db.prepare('SELECT COUNT(*) AS cnt FROM edges').get().cnt;
119
+ }
120
+
121
+ /**
122
+ * Count distinct files.
123
+ * @param {object} db
124
+ * @returns {number}
125
+ */
126
+ export function countFiles(db) {
127
+ return db.prepare('SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt;
128
+ }
129
+
130
+ // ─── Shared node lookups ───────────────────────────────────────────────
131
+
132
+ /**
133
+ * Find a single node by ID.
134
+ * @param {object} db
135
+ * @param {number} id
136
+ * @returns {object|undefined}
137
+ */
138
+ export function findNodeById(db, id) {
139
+ return db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
140
+ }
141
+
142
+ /**
143
+ * Find non-file nodes for a given file path (exact match), ordered by line.
144
+ * @param {object} db
145
+ * @param {string} file - Exact file path
146
+ * @returns {object[]}
147
+ */
148
+ export function findNodesByFile(db, file) {
149
+ return db
150
+ .prepare("SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line")
151
+ .all(file);
152
+ }
153
+
154
+ /**
155
+ * Find file-kind nodes matching a LIKE pattern.
156
+ * @param {object} db
157
+ * @param {string} fileLike - LIKE pattern (caller wraps with %)
158
+ * @returns {object[]}
159
+ */
160
+ export function findFileNodes(db, fileLike) {
161
+ return db.prepare("SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'").all(fileLike);
162
+ }
163
+
164
+ // ─── Statement caches (one prepared statement per db instance) ────────────
165
+ // WeakMap keys on the db object so statements are GC'd when the db closes.
166
+ const _getNodeIdStmt = new WeakMap();
167
+ const _getFunctionNodeIdStmt = new WeakMap();
168
+ const _bulkNodeIdsByFileStmt = new WeakMap();
169
+
170
+ /**
171
+ * Look up a node's ID by its unique (name, kind, file, line) tuple.
172
+ * Shared by builder, watcher, structure, complexity, cfg, engine.
173
+ * @param {object} db
174
+ * @param {string} name
175
+ * @param {string} kind
176
+ * @param {string} file
177
+ * @param {number} line
178
+ * @returns {number|undefined}
179
+ */
180
+ export function getNodeId(db, name, kind, file, line) {
181
+ let stmt = _getNodeIdStmt.get(db);
182
+ if (!stmt) {
183
+ stmt = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?');
184
+ _getNodeIdStmt.set(db, stmt);
185
+ }
186
+ return stmt.get(name, kind, file, line)?.id;
187
+ }
188
+
189
+ /**
190
+ * Look up a function/method node's ID (kind-restricted variant of getNodeId).
191
+ * Used by complexity.js, cfg.js where only function/method kinds are expected.
192
+ * @param {object} db
193
+ * @param {string} name
194
+ * @param {string} file
195
+ * @param {number} line
196
+ * @returns {number|undefined}
197
+ */
198
+ export function getFunctionNodeId(db, name, file, line) {
199
+ let stmt = _getFunctionNodeIdStmt.get(db);
200
+ if (!stmt) {
201
+ stmt = db.prepare(
202
+ "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?",
203
+ );
204
+ _getFunctionNodeIdStmt.set(db, stmt);
205
+ }
206
+ return stmt.get(name, file, line)?.id;
207
+ }
208
+
209
+ /**
210
+ * Bulk-fetch all node IDs for a file in one query.
211
+ * Returns rows suitable for building a `name|kind|line -> id` lookup map.
212
+ * Shared by builder, ast.js, ast-analysis/engine.js.
213
+ * @param {object} db
214
+ * @param {string} file
215
+ * @returns {{ id: number, name: string, kind: string, line: number }[]}
216
+ */
217
+ export function bulkNodeIdsByFile(db, file) {
218
+ let stmt = _bulkNodeIdsByFileStmt.get(db);
219
+ if (!stmt) {
220
+ stmt = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?');
221
+ _bulkNodeIdsByFileStmt.set(db, stmt);
222
+ }
223
+ return stmt.all(file);
224
+ }
225
+
226
+ /**
227
+ * Find child nodes (parameters, properties, constants) of a parent.
228
+ * @param {object} db
229
+ * @param {number} parentId
230
+ * @returns {{ name: string, kind: string, line: number, end_line: number|null }[]}
231
+ */
232
+ export function findNodeChildren(db, parentId) {
233
+ return db
234
+ .prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line')
235
+ .all(parentId);
236
+ }
package/src/db.js CHANGED
@@ -9,11 +9,50 @@ export {
9
9
  testFilterSQL,
10
10
  } from './db/query-builder.js';
11
11
  export {
12
+ bulkNodeIdsByFile,
13
+ countCrossFileCallers,
12
14
  countEdges,
13
15
  countFiles,
14
16
  countNodes,
17
+ deleteCfgForNode,
18
+ findAllIncomingEdges,
19
+ findAllOutgoingEdges,
20
+ findCalleeNames,
21
+ findCallees,
22
+ findCallerNames,
23
+ findCallers,
24
+ findCrossFileCallTargets,
25
+ findDistinctCallers,
26
+ findFileNodes,
27
+ findImportDependents,
28
+ findImportSources,
29
+ findImportTargets,
30
+ findIntraFileCallEdges,
31
+ findNodeById,
32
+ findNodeChildren,
33
+ findNodesByFile,
15
34
  findNodesForTriage,
16
35
  findNodesWithFanIn,
36
+ getCallableNodes,
37
+ getCallEdges,
38
+ getCfgBlocks,
39
+ getCfgEdges,
40
+ getClassHierarchy,
41
+ getCoChangeMeta,
42
+ getComplexityForNode,
43
+ getEmbeddingCount,
44
+ getEmbeddingMeta,
45
+ getFileNodesAll,
46
+ getFunctionNodeId,
47
+ getImportEdges,
48
+ getNodeId,
49
+ hasCfgTables,
50
+ hasCoChanges,
51
+ hasDataflowTable,
52
+ hasEmbeddings,
17
53
  iterateFunctionNodes,
18
54
  listFunctionNodes,
19
- } from './db/repository.js';
55
+ purgeFileData,
56
+ purgeFilesData,
57
+ upsertCoChangeMeta,
58
+ } from './db/repository/index.js';
package/src/embedder.js CHANGED
@@ -2,7 +2,14 @@ import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { createInterface } from 'node:readline';
5
- import { closeDb, findDbPath, openDb, openReadonlyOrFail } from './db.js';
5
+ import {
6
+ closeDb,
7
+ findCalleeNames,
8
+ findCallerNames,
9
+ findDbPath,
10
+ openDb,
11
+ openReadonlyOrFail,
12
+ } from './db.js';
6
13
  import { info, warn } from './logger.js';
7
14
  import { normalizeSymbol } from './queries.js';
8
15
 
@@ -166,7 +173,7 @@ function extractLeadingComment(lines, fnLineIndex) {
166
173
  * Build graph-enriched text for a symbol using dependency context.
167
174
  * Produces compact, semantic text (~100 tokens) instead of full source code.
168
175
  */
169
- function buildStructuredText(node, file, lines, calleesStmt, callersStmt) {
176
+ function buildStructuredText(node, file, lines, db) {
170
177
  const readable = splitIdentifier(node.name);
171
178
  const parts = [`${node.kind} ${node.name} (${readable}) in ${file}`];
172
179
  const startLine = Math.max(0, node.line - 1);
@@ -179,25 +186,15 @@ function buildStructuredText(node, file, lines, calleesStmt, callersStmt) {
179
186
  }
180
187
 
181
188
  // Graph context: callees (capped at 10)
182
- const callees = calleesStmt.all(node.id);
189
+ const callees = findCalleeNames(db, node.id);
183
190
  if (callees.length > 0) {
184
- parts.push(
185
- `Calls: ${callees
186
- .slice(0, 10)
187
- .map((c) => c.name)
188
- .join(', ')}`,
189
- );
191
+ parts.push(`Calls: ${callees.slice(0, 10).join(', ')}`);
190
192
  }
191
193
 
192
194
  // Graph context: callers (capped at 10)
193
- const callers = callersStmt.all(node.id);
195
+ const callers = findCallerNames(db, node.id);
194
196
  if (callers.length > 0) {
195
- parts.push(
196
- `Called by: ${callers
197
- .slice(0, 10)
198
- .map((c) => c.name)
199
- .join(', ')}`,
200
- );
197
+ parts.push(`Called by: ${callers.slice(0, 10).join(', ')}`);
201
198
  }
202
199
 
203
200
  // Leading comment (high semantic value) or first few lines of code
@@ -438,23 +435,6 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath, options =
438
435
 
439
436
  console.log(`Building embeddings for ${nodes.length} symbols (strategy: ${strategy})...`);
440
437
 
441
- // Prepare graph-context queries for structured strategy
442
- let calleesStmt, callersStmt;
443
- if (strategy === 'structured') {
444
- calleesStmt = db.prepare(`
445
- SELECT DISTINCT n.name FROM edges e
446
- JOIN nodes n ON e.target_id = n.id
447
- WHERE e.source_id = ? AND e.kind = 'calls'
448
- ORDER BY n.name
449
- `);
450
- callersStmt = db.prepare(`
451
- SELECT DISTINCT n.name FROM edges e
452
- JOIN nodes n ON e.source_id = n.id
453
- WHERE e.target_id = ? AND e.kind = 'calls'
454
- ORDER BY n.name
455
- `);
456
- }
457
-
458
438
  const byFile = new Map();
459
439
  for (const node of nodes) {
460
440
  if (!byFile.has(node.file)) byFile.set(node.file, []);
@@ -482,7 +462,7 @@ export async function buildEmbeddings(rootDir, modelKey, customDbPath, options =
482
462
  for (const node of fileNodes) {
483
463
  let text =
484
464
  strategy === 'structured'
485
- ? buildStructuredText(node, file, lines, calleesStmt, callersStmt)
465
+ ? buildStructuredText(node, file, lines, db)
486
466
  : buildSourceText(node, file, lines);
487
467
 
488
468
  // Detect and handle context window overflow
package/src/export.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
+ import { isTestFile } from './infrastructure/test-filter.js';
2
3
  import { paginateResult } from './paginate.js';
3
- import { isTestFile } from './test-filter.js';
4
4
 
5
5
  const DEFAULT_MIN_CONFIDENCE = 0.5;
6
6
 
@@ -1,3 +1,4 @@
1
+ import { debug } from '../logger.js';
1
2
  import { findChild, nodeEndLine } from './helpers.js';
2
3
 
3
4
  /**
@@ -173,6 +174,9 @@ function extractSymbolsQuery(tree, query) {
173
174
  // Extract top-level constants via targeted walk (query patterns don't cover these)
174
175
  extractConstantsWalk(tree.rootNode, definitions);
175
176
 
177
+ // Extract dynamic import() calls via targeted walk (query patterns don't match `import` function type)
178
+ extractDynamicImportsWalk(tree.rootNode, imports);
179
+
176
180
  return { definitions, calls, imports, classes, exports: exps };
177
181
  }
178
182
 
@@ -224,6 +228,41 @@ function extractConstantsWalk(rootNode, definitions) {
224
228
  }
225
229
  }
226
230
 
231
+ /**
232
+ * Recursive walk to find dynamic import() calls.
233
+ * Query patterns match call_expression with identifier/member_expression/subscript_expression
234
+ * functions, but import() has function type `import` which none of those patterns cover.
235
+ */
236
+ function extractDynamicImportsWalk(node, imports) {
237
+ if (node.type === 'call_expression') {
238
+ const fn = node.childForFieldName('function');
239
+ if (fn && fn.type === 'import') {
240
+ const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
241
+ if (args) {
242
+ const strArg = findChild(args, 'string');
243
+ if (strArg) {
244
+ const modPath = strArg.text.replace(/['"]/g, '');
245
+ const names = extractDynamicImportNames(node);
246
+ imports.push({
247
+ source: modPath,
248
+ names,
249
+ line: node.startPosition.row + 1,
250
+ dynamicImport: true,
251
+ });
252
+ } else {
253
+ debug(
254
+ `Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`,
255
+ );
256
+ }
257
+ }
258
+ return; // no need to recurse into import() children
259
+ }
260
+ }
261
+ for (let i = 0; i < node.childCount; i++) {
262
+ extractDynamicImportsWalk(node.child(i), imports);
263
+ }
264
+ }
265
+
227
266
  function handleCommonJSAssignment(left, right, node, imports) {
228
267
  if (!left || !right) return;
229
268
  const leftText = left.text;
@@ -455,11 +494,36 @@ function extractSymbolsWalk(tree) {
455
494
  case 'call_expression': {
456
495
  const fn = node.childForFieldName('function');
457
496
  if (fn) {
458
- const callInfo = extractCallInfo(fn, node);
459
- if (callInfo) calls.push(callInfo);
460
- if (fn.type === 'member_expression') {
461
- const cbDef = extractCallbackDefinition(node, fn);
462
- if (cbDef) definitions.push(cbDef);
497
+ // Dynamic import(): import('./foo.js') → extract as an import entry
498
+ if (fn.type === 'import') {
499
+ const args = node.childForFieldName('arguments') || findChild(node, 'arguments');
500
+ if (args) {
501
+ const strArg = findChild(args, 'string');
502
+ if (strArg) {
503
+ const modPath = strArg.text.replace(/['"]/g, '');
504
+ // Extract destructured names from parent context:
505
+ // const { a, b } = await import('./foo.js')
506
+ // (standalone import('./foo.js').then(...) calls produce an edge with empty names)
507
+ const names = extractDynamicImportNames(node);
508
+ imports.push({
509
+ source: modPath,
510
+ names,
511
+ line: node.startPosition.row + 1,
512
+ dynamicImport: true,
513
+ });
514
+ } else {
515
+ debug(
516
+ `Skipping non-static dynamic import() at line ${node.startPosition.row + 1} (template literal or variable)`,
517
+ );
518
+ }
519
+ }
520
+ } else {
521
+ const callInfo = extractCallInfo(fn, node);
522
+ if (callInfo) calls.push(callInfo);
523
+ if (fn.type === 'member_expression') {
524
+ const cbDef = extractCallbackDefinition(node, fn);
525
+ if (cbDef) definitions.push(cbDef);
526
+ }
463
527
  }
464
528
  }
465
529
  break;
@@ -941,3 +1005,64 @@ function extractImportNames(node) {
941
1005
  scan(node);
942
1006
  return names;
943
1007
  }
1008
+
1009
+ /**
1010
+ * Extract destructured names from a dynamic import() call expression.
1011
+ *
1012
+ * Handles:
1013
+ * const { a, b } = await import('./foo.js') → ['a', 'b']
1014
+ * const mod = await import('./foo.js') → ['mod']
1015
+ * import('./foo.js') → [] (no names extractable)
1016
+ *
1017
+ * Walks up the AST from the call_expression to find the enclosing
1018
+ * variable_declarator and reads the name/object_pattern.
1019
+ */
1020
+ function extractDynamicImportNames(callNode) {
1021
+ // Walk up: call_expression → await_expression → variable_declarator
1022
+ let current = callNode.parent;
1023
+ // Skip await_expression wrapper if present
1024
+ if (current && current.type === 'await_expression') current = current.parent;
1025
+ // We should now be at a variable_declarator (or not, if standalone import())
1026
+ if (!current || current.type !== 'variable_declarator') return [];
1027
+
1028
+ const nameNode = current.childForFieldName('name');
1029
+ if (!nameNode) return [];
1030
+
1031
+ // const { a, b } = await import(...) → object_pattern
1032
+ if (nameNode.type === 'object_pattern') {
1033
+ const names = [];
1034
+ for (let i = 0; i < nameNode.childCount; i++) {
1035
+ const child = nameNode.child(i);
1036
+ if (child.type === 'shorthand_property_identifier_pattern') {
1037
+ names.push(child.text);
1038
+ } else if (child.type === 'pair_pattern') {
1039
+ // { a: localName } → use localName (the alias) for the local binding,
1040
+ // but use the key (original name) for import resolution
1041
+ const key = child.childForFieldName('key');
1042
+ if (key) names.push(key.text);
1043
+ }
1044
+ }
1045
+ return names;
1046
+ }
1047
+
1048
+ // const mod = await import(...) → identifier (namespace-like import)
1049
+ if (nameNode.type === 'identifier') {
1050
+ return [nameNode.text];
1051
+ }
1052
+
1053
+ // const [a, b] = await import(...) → array_pattern (rare but possible)
1054
+ if (nameNode.type === 'array_pattern') {
1055
+ const names = [];
1056
+ for (let i = 0; i < nameNode.childCount; i++) {
1057
+ const child = nameNode.child(i);
1058
+ if (child.type === 'identifier') names.push(child.text);
1059
+ else if (child.type === 'rest_pattern') {
1060
+ const inner = child.child(0) || child.childForFieldName('name');
1061
+ if (inner && inner.type === 'identifier') names.push(inner.text);
1062
+ }
1063
+ }
1064
+ return names;
1065
+ }
1066
+
1067
+ return [];
1068
+ }
package/src/flow.js CHANGED
@@ -6,11 +6,10 @@
6
6
  */
7
7
 
8
8
  import { openReadonlyOrFail } from './db.js';
9
+ import { isTestFile } from './infrastructure/test-filter.js';
9
10
  import { paginateResult } from './paginate.js';
10
- import { CORE_SYMBOL_KINDS, findMatchingNodes, kindIcon } from './queries.js';
11
- import { outputResult } from './result-formatter.js';
11
+ import { CORE_SYMBOL_KINDS, findMatchingNodes } from './queries.js';
12
12
  import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
13
- import { isTestFile } from './test-filter.js';
14
13
 
15
14
  /**
16
15
  * Determine the entry point type from a node name based on framework prefixes.
@@ -227,70 +226,3 @@ export function flowData(name, dbPath, opts = {}) {
227
226
  db.close();
228
227
  }
229
228
  }
230
-
231
- /**
232
- * CLI formatter — text or JSON output.
233
- */
234
- export function flow(name, dbPath, opts = {}) {
235
- if (opts.list) {
236
- const data = listEntryPointsData(dbPath, {
237
- noTests: opts.noTests,
238
- limit: opts.limit,
239
- offset: opts.offset,
240
- });
241
- if (outputResult(data, 'entries', opts)) return;
242
- if (data.count === 0) {
243
- console.log('No entry points found. Run "codegraph build" first.');
244
- return;
245
- }
246
- console.log(`\nEntry points (${data.count} total):\n`);
247
- for (const [type, entries] of Object.entries(data.byType)) {
248
- console.log(` ${type} (${entries.length}):`);
249
- for (const e of entries) {
250
- console.log(` [${kindIcon(e.kind)}] ${e.name} ${e.file}:${e.line}`);
251
- }
252
- console.log();
253
- }
254
- return;
255
- }
256
-
257
- const data = flowData(name, dbPath, opts);
258
- if (outputResult(data, 'steps', opts)) return;
259
-
260
- if (!data.entry) {
261
- console.log(`No matching entry point or function found for "${name}".`);
262
- return;
263
- }
264
-
265
- const e = data.entry;
266
- const typeTag = e.type !== 'exported' ? ` (${e.type})` : '';
267
- console.log(`\nFlow from: [${kindIcon(e.kind)}] ${e.name}${typeTag} ${e.file}:${e.line}`);
268
- console.log(
269
- `Depth: ${data.depth} Reached: ${data.totalReached} nodes Leaves: ${data.leaves.length}`,
270
- );
271
- if (data.truncated) {
272
- console.log(` (truncated at depth ${data.depth})`);
273
- }
274
- console.log();
275
-
276
- if (data.steps.length === 0) {
277
- console.log(' (leaf node — no callees)');
278
- return;
279
- }
280
-
281
- for (const step of data.steps) {
282
- console.log(` depth ${step.depth}:`);
283
- for (const n of step.nodes) {
284
- const isLeaf = data.leaves.some((l) => l.name === n.name && l.file === n.file);
285
- const leafTag = isLeaf ? ' [leaf]' : '';
286
- console.log(` [${kindIcon(n.kind)}] ${n.name} ${n.file}:${n.line}${leafTag}`);
287
- }
288
- }
289
-
290
- if (data.cycles.length > 0) {
291
- console.log('\n Cycles detected:');
292
- for (const c of data.cycles) {
293
- console.log(` ${c.from} -> ${c.to} (at depth ${c.depth})`);
294
- }
295
- }
296
- }