@optave/codegraph 3.1.5 → 3.2.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.
Files changed (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -12,10 +12,121 @@ import { parseFileIncremental } from '../../parser.js';
12
12
  import { computeConfidence, resolveImportPath } from '../resolve.js';
13
13
  import { BUILTIN_RECEIVERS, readFileSafe } from './helpers.js';
14
14
 
15
+ // ── Node insertion ──────────────────────────────────────────────────────
16
+
17
+ function insertFileNodes(stmts, relPath, symbols) {
18
+ stmts.insertNode.run(relPath, 'file', relPath, 0, null);
19
+ for (const def of symbols.definitions) {
20
+ stmts.insertNode.run(def.name, def.kind, relPath, def.line, def.endLine || null);
21
+ }
22
+ for (const exp of symbols.exports) {
23
+ stmts.insertNode.run(exp.name, exp.kind, relPath, exp.line, null);
24
+ }
25
+ }
26
+
27
+ // ── Import edge building ────────────────────────────────────────────────
28
+
29
+ function buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeId, aliases) {
30
+ let edgesAdded = 0;
31
+ for (const imp of symbols.imports) {
32
+ const resolvedPath = resolveImportPath(
33
+ path.join(rootDir, relPath),
34
+ imp.source,
35
+ rootDir,
36
+ aliases,
37
+ );
38
+ const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
39
+ if (targetRow) {
40
+ const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
41
+ stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
42
+ edgesAdded++;
43
+ }
44
+ }
45
+ return edgesAdded;
46
+ }
47
+
48
+ function buildImportedNamesMap(symbols, rootDir, relPath, aliases) {
49
+ const importedNames = new Map();
50
+ for (const imp of symbols.imports) {
51
+ const resolvedPath = resolveImportPath(
52
+ path.join(rootDir, relPath),
53
+ imp.source,
54
+ rootDir,
55
+ aliases,
56
+ );
57
+ for (const name of imp.names) {
58
+ importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
59
+ }
60
+ }
61
+ return importedNames;
62
+ }
63
+
64
+ // ── Call edge building ──────────────────────────────────────────────────
65
+
66
+ function findCaller(call, definitions, relPath, stmts) {
67
+ let caller = null;
68
+ let callerSpan = Infinity;
69
+ for (const def of definitions) {
70
+ if (def.line <= call.line) {
71
+ const end = def.endLine || Infinity;
72
+ if (call.line <= end) {
73
+ const span = end - def.line;
74
+ if (span < callerSpan) {
75
+ const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
76
+ if (row) {
77
+ caller = row;
78
+ callerSpan = span;
79
+ }
80
+ }
81
+ } else if (!caller) {
82
+ const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
83
+ if (row) caller = row;
84
+ }
85
+ }
86
+ }
87
+ return caller;
88
+ }
89
+
90
+ function resolveCallTargets(stmts, call, relPath, importedNames) {
91
+ const importedFrom = importedNames.get(call.name);
92
+ let targets;
93
+ if (importedFrom) {
94
+ targets = stmts.findNodeInFile.all(call.name, importedFrom);
95
+ }
96
+ if (!targets || targets.length === 0) {
97
+ targets = stmts.findNodeInFile.all(call.name, relPath);
98
+ if (targets.length === 0) {
99
+ targets = stmts.findNodeByName.all(call.name);
100
+ }
101
+ }
102
+ return { targets, importedFrom };
103
+ }
104
+
105
+ function buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames) {
106
+ let edgesAdded = 0;
107
+ for (const call of symbols.calls) {
108
+ if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
109
+
110
+ const caller = findCaller(call, symbols.definitions, relPath, stmts) || fileNodeRow;
111
+ const { targets, importedFrom } = resolveCallTargets(stmts, call, relPath, importedNames);
112
+
113
+ for (const t of targets) {
114
+ if (t.id !== caller.id) {
115
+ const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
116
+ stmts.insertEdge.run(caller.id, t.id, 'calls', confidence, call.dynamic ? 1 : 0);
117
+ edgesAdded++;
118
+ }
119
+ }
120
+ }
121
+ return edgesAdded;
122
+ }
123
+
124
+ // ── Main entry point ────────────────────────────────────────────────────
125
+
15
126
  /**
16
127
  * Parse a single file and update the database incrementally.
17
128
  *
18
- * @param {import('better-sqlite3').Database} db
129
+ * @param {import('better-sqlite3').Database} _db
19
130
  * @param {string} rootDir - Absolute root directory
20
131
  * @param {string} filePath - Absolute file path
21
132
  * @param {object} stmts - Prepared DB statements
@@ -61,105 +172,20 @@ export async function rebuildFile(_db, rootDir, filePath, stmts, engineOpts, cac
61
172
  const symbols = await parseFileIncremental(cache, filePath, code, engineOpts);
62
173
  if (!symbols) return null;
63
174
 
64
- // Insert nodes
65
- stmts.insertNode.run(relPath, 'file', relPath, 0, null);
66
- for (const def of symbols.definitions) {
67
- stmts.insertNode.run(def.name, def.kind, relPath, def.line, def.endLine || null);
68
- }
69
- for (const exp of symbols.exports) {
70
- stmts.insertNode.run(exp.name, exp.kind, relPath, exp.line, null);
71
- }
175
+ insertFileNodes(stmts, relPath, symbols);
72
176
 
73
177
  const newNodes = stmts.countNodes.get(relPath)?.c || 0;
74
178
  const newSymbols = diffSymbols ? stmts.listSymbols.all(relPath) : [];
75
179
 
76
- let edgesAdded = 0;
77
180
  const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
78
181
  if (!fileNodeRow)
79
182
  return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
80
- const fileNodeId = fileNodeRow.id;
81
183
 
82
- // Load aliases for import resolution
83
184
  const aliases = { baseUrl: null, paths: {} };
84
185
 
85
- // Import edges
86
- for (const imp of symbols.imports) {
87
- const resolvedPath = resolveImportPath(
88
- path.join(rootDir, relPath),
89
- imp.source,
90
- rootDir,
91
- aliases,
92
- );
93
- const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
94
- if (targetRow) {
95
- const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
96
- stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
97
- edgesAdded++;
98
- }
99
- }
100
-
101
- // Build import name → resolved file mapping
102
- const importedNames = new Map();
103
- for (const imp of symbols.imports) {
104
- const resolvedPath = resolveImportPath(
105
- path.join(rootDir, relPath),
106
- imp.source,
107
- rootDir,
108
- aliases,
109
- );
110
- for (const name of imp.names) {
111
- importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
112
- }
113
- }
114
-
115
- // Call edges
116
- for (const call of symbols.calls) {
117
- if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
118
-
119
- let caller = null;
120
- let callerSpan = Infinity;
121
- for (const def of symbols.definitions) {
122
- if (def.line <= call.line) {
123
- const end = def.endLine || Infinity;
124
- if (call.line <= end) {
125
- const span = end - def.line;
126
- if (span < callerSpan) {
127
- const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
128
- if (row) {
129
- caller = row;
130
- callerSpan = span;
131
- }
132
- }
133
- } else if (!caller) {
134
- const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
135
- if (row) caller = row;
136
- }
137
- }
138
- }
139
- if (!caller) caller = fileNodeRow;
140
-
141
- const importedFrom = importedNames.get(call.name);
142
- let targets;
143
- if (importedFrom) {
144
- targets = stmts.findNodeInFile.all(call.name, importedFrom);
145
- }
146
- if (!targets || targets.length === 0) {
147
- targets = stmts.findNodeInFile.all(call.name, relPath);
148
- if (targets.length === 0) {
149
- targets = stmts.findNodeByName.all(call.name);
150
- }
151
- }
152
-
153
- for (const t of targets) {
154
- if (t.id !== caller.id) {
155
- const confidence = importedFrom
156
- ? computeConfidence(relPath, t.file, importedFrom)
157
- : computeConfidence(relPath, t.file, null);
158
- stmts.insertEdge.run(caller.id, t.id, 'calls', confidence, call.dynamic ? 1 : 0);
159
- edgesAdded++;
160
- }
161
- }
162
- }
186
+ let edgesAdded = buildImportEdges(stmts, relPath, symbols, rootDir, fileNodeRow.id, aliases);
187
+ const importedNames = buildImportedNamesMap(symbols, rootDir, relPath, aliases);
188
+ edgesAdded += buildCallEdges(stmts, relPath, symbols, fileNodeRow, importedNames);
163
189
 
164
190
  const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, newSymbols) : null;
165
191
  const event = oldNodes === 0 ? 'added' : 'modified';
@@ -23,94 +23,73 @@ import { parseFiles } from './stages/parse-files.js';
23
23
  import { resolveImports } from './stages/resolve-imports.js';
24
24
  import { runAnalyses } from './stages/run-analyses.js';
25
25
 
26
- /**
27
- * Build the dependency graph for a codebase.
28
- *
29
- * Signature and return value are identical to the original monolithic buildGraph().
30
- *
31
- * @param {string} rootDir - Root directory to scan
32
- * @param {object} [opts] - Build options
33
- * @returns {Promise<{ phases: object } | undefined>}
34
- */
35
- export async function buildGraph(rootDir, opts = {}) {
36
- const ctx = new PipelineContext();
37
- ctx.buildStart = performance.now();
38
- ctx.opts = opts;
26
+ // ── Setup helpers ───────────────────────────────────────────────────────
39
27
 
40
- // ── Setup (creates DB, loads config, selects engine) ──────────────
41
- ctx.rootDir = path.resolve(rootDir);
42
- ctx.dbPath = path.join(ctx.rootDir, '.codegraph', 'graph.db');
43
- ctx.db = openDb(ctx.dbPath);
44
- try {
45
- initSchema(ctx.db);
46
-
47
- ctx.config = loadConfig(ctx.rootDir);
48
- ctx.incremental =
49
- opts.incremental !== false && ctx.config.build && ctx.config.build.incremental !== false;
50
-
51
- ctx.engineOpts = {
52
- engine: opts.engine || 'auto',
53
- dataflow: opts.dataflow !== false,
54
- ast: opts.ast !== false,
55
- };
56
- const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
57
- ctx.engineName = engineName;
58
- ctx.engineVersion = engineVersion;
59
- info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
60
-
61
- // Engine/schema mismatch detection
62
- ctx.schemaVersion = MIGRATIONS[MIGRATIONS.length - 1].version;
63
- ctx.forceFullRebuild = false;
64
- if (ctx.incremental) {
65
- const prevEngine = getBuildMeta(ctx.db, 'engine');
66
- if (prevEngine && prevEngine !== engineName) {
67
- info(`Engine changed (${prevEngine} → ${engineName}), promoting to full rebuild.`);
68
- ctx.forceFullRebuild = true;
69
- }
70
- const prevSchema = getBuildMeta(ctx.db, 'schema_version');
71
- if (prevSchema && Number(prevSchema) !== ctx.schemaVersion) {
72
- info(
73
- `Schema version changed (${prevSchema} → ${ctx.schemaVersion}), promoting to full rebuild.`,
74
- );
75
- ctx.forceFullRebuild = true;
76
- }
77
- }
28
+ function initializeEngine(ctx) {
29
+ ctx.engineOpts = {
30
+ engine: ctx.opts.engine || 'auto',
31
+ dataflow: ctx.opts.dataflow !== false,
32
+ ast: ctx.opts.ast !== false,
33
+ };
34
+ const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
35
+ ctx.engineName = engineName;
36
+ ctx.engineVersion = engineVersion;
37
+ info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
38
+ }
78
39
 
79
- // Path aliases
80
- ctx.aliases = loadPathAliases(ctx.rootDir);
81
- if (ctx.config.aliases) {
82
- for (const [key, value] of Object.entries(ctx.config.aliases)) {
83
- const pattern = key.endsWith('/') ? `${key}*` : key;
84
- const target = path.resolve(ctx.rootDir, value);
85
- ctx.aliases.paths[pattern] = [target.endsWith('/') ? `${target}*` : `${target}/*`];
86
- }
87
- }
88
- if (ctx.aliases.baseUrl || Object.keys(ctx.aliases.paths).length > 0) {
89
- info(
90
- `Loaded path aliases: baseUrl=${ctx.aliases.baseUrl || 'none'}, ${Object.keys(ctx.aliases.paths).length} path mappings`,
91
- );
40
+ function checkEngineSchemaMismatch(ctx) {
41
+ ctx.schemaVersion = MIGRATIONS[MIGRATIONS.length - 1].version;
42
+ ctx.forceFullRebuild = false;
43
+ if (!ctx.incremental) return;
44
+
45
+ const prevEngine = getBuildMeta(ctx.db, 'engine');
46
+ if (prevEngine && prevEngine !== ctx.engineName) {
47
+ info(`Engine changed (${prevEngine} → ${ctx.engineName}), promoting to full rebuild.`);
48
+ ctx.forceFullRebuild = true;
49
+ }
50
+ const prevSchema = getBuildMeta(ctx.db, 'schema_version');
51
+ if (prevSchema && Number(prevSchema) !== ctx.schemaVersion) {
52
+ info(
53
+ `Schema version changed (${prevSchema} → ${ctx.schemaVersion}), promoting to full rebuild.`,
54
+ );
55
+ ctx.forceFullRebuild = true;
56
+ }
57
+ }
58
+
59
+ function loadAliases(ctx) {
60
+ ctx.aliases = loadPathAliases(ctx.rootDir);
61
+ if (ctx.config.aliases) {
62
+ for (const [key, value] of Object.entries(ctx.config.aliases)) {
63
+ const pattern = key.endsWith('/') ? `${key}*` : key;
64
+ const target = path.resolve(ctx.rootDir, value);
65
+ ctx.aliases.paths[pattern] = [target.endsWith('/') ? `${target}*` : `${target}/*`];
92
66
  }
67
+ }
68
+ if (ctx.aliases.baseUrl || Object.keys(ctx.aliases.paths).length > 0) {
69
+ info(
70
+ `Loaded path aliases: baseUrl=${ctx.aliases.baseUrl || 'none'}, ${Object.keys(ctx.aliases.paths).length} path mappings`,
71
+ );
72
+ }
73
+ }
93
74
 
94
- ctx.timing.setupMs = performance.now() - ctx.buildStart;
75
+ function setupPipeline(ctx) {
76
+ ctx.rootDir = path.resolve(ctx.rootDir);
77
+ ctx.dbPath = path.join(ctx.rootDir, '.codegraph', 'graph.db');
78
+ ctx.db = openDb(ctx.dbPath);
79
+ initSchema(ctx.db);
95
80
 
96
- // ── Pipeline stages ─────────────────────────────────────────────
97
- await collectFiles(ctx);
98
- await detectChanges(ctx);
81
+ ctx.config = loadConfig(ctx.rootDir);
82
+ ctx.incremental =
83
+ ctx.opts.incremental !== false && ctx.config.build && ctx.config.build.incremental !== false;
99
84
 
100
- if (ctx.earlyExit) return;
85
+ initializeEngine(ctx);
86
+ checkEngineSchemaMismatch(ctx);
87
+ loadAliases(ctx);
101
88
 
102
- await parseFiles(ctx);
103
- await insertNodes(ctx);
104
- await resolveImports(ctx);
105
- await buildEdges(ctx);
106
- await buildStructure(ctx);
107
- await runAnalyses(ctx);
108
- await finalize(ctx);
109
- } catch (err) {
110
- if (!ctx.earlyExit) closeDb(ctx.db);
111
- throw err;
112
- }
89
+ ctx.timing.setupMs = performance.now() - ctx.buildStart;
90
+ }
113
91
 
92
+ function formatTimingResult(ctx) {
114
93
  return {
115
94
  phases: {
116
95
  setupMs: +ctx.timing.setupMs.toFixed(1),
@@ -128,3 +107,50 @@ export async function buildGraph(rootDir, opts = {}) {
128
107
  },
129
108
  };
130
109
  }
110
+
111
+ // ── Pipeline stages execution ───────────────────────────────────────────
112
+
113
+ async function runPipelineStages(ctx) {
114
+ await collectFiles(ctx);
115
+ await detectChanges(ctx);
116
+
117
+ if (ctx.earlyExit) return;
118
+
119
+ await parseFiles(ctx);
120
+ await insertNodes(ctx);
121
+ await resolveImports(ctx);
122
+ await buildEdges(ctx);
123
+ await buildStructure(ctx);
124
+ await runAnalyses(ctx);
125
+ await finalize(ctx);
126
+ }
127
+
128
+ // ── Main entry point ────────────────────────────────────────────────────
129
+
130
+ /**
131
+ * Build the dependency graph for a codebase.
132
+ *
133
+ * Signature and return value are identical to the original monolithic buildGraph().
134
+ *
135
+ * @param {string} rootDir - Root directory to scan
136
+ * @param {object} [opts] - Build options
137
+ * @returns {Promise<{ phases: object } | undefined>}
138
+ */
139
+ export async function buildGraph(rootDir, opts = {}) {
140
+ const ctx = new PipelineContext();
141
+ ctx.buildStart = performance.now();
142
+ ctx.opts = opts;
143
+ ctx.rootDir = rootDir;
144
+
145
+ try {
146
+ setupPipeline(ctx);
147
+ await runPipelineStages(ctx);
148
+ } catch (err) {
149
+ if (!ctx.earlyExit) closeDb(ctx.db);
150
+ throw err;
151
+ }
152
+
153
+ if (ctx.earlyExit) return;
154
+
155
+ return formatTimingResult(ctx);
156
+ }