@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
package/src/mcp/server.js CHANGED
@@ -21,45 +21,84 @@ import { TOOL_HANDLERS } from './tools/index.js';
21
21
  * @param {boolean} [options.multiRepo] - Enable multi-repo access (default: false)
22
22
  * @param {string[]} [options.allowedRepos] - Restrict access to these repo names only
23
23
  */
24
- export async function startMCPServer(customDbPath, options = {}) {
25
- const { allowedRepos } = options;
26
- const multiRepo = options.multiRepo || !!allowedRepos;
27
- let Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema;
24
+ async function loadMCPSdk() {
28
25
  try {
29
26
  const sdk = await import('@modelcontextprotocol/sdk/server/index.js');
30
- Server = sdk.Server;
31
27
  const transport = await import('@modelcontextprotocol/sdk/server/stdio.js');
32
- StdioServerTransport = transport.StdioServerTransport;
33
28
  const types = await import('@modelcontextprotocol/sdk/types.js');
34
- ListToolsRequestSchema = types.ListToolsRequestSchema;
35
- CallToolRequestSchema = types.CallToolRequestSchema;
29
+ return {
30
+ Server: sdk.Server,
31
+ StdioServerTransport: transport.StdioServerTransport,
32
+ ListToolsRequestSchema: types.ListToolsRequestSchema,
33
+ CallToolRequestSchema: types.CallToolRequestSchema,
34
+ };
36
35
  } catch {
37
36
  throw new ConfigError(
38
37
  'MCP server requires @modelcontextprotocol/sdk.\nInstall it with: npm install @modelcontextprotocol/sdk',
39
38
  );
40
39
  }
40
+ }
41
41
 
42
- // Connect transport FIRST so the server can receive the client's
43
- // `initialize` request while heavy modules (queries, better-sqlite3)
44
- // are still loading. These are lazy-loaded on the first tool call
45
- // and cached for subsequent calls.
42
+ function createLazyLoaders() {
46
43
  let _queries;
47
44
  let _Database;
45
+ return {
46
+ async getQueries() {
47
+ if (!_queries) _queries = await import('../domain/queries.js');
48
+ return _queries;
49
+ },
50
+ getDatabase() {
51
+ if (!_Database) {
52
+ const require = createRequire(import.meta.url);
53
+ _Database = require('better-sqlite3');
54
+ }
55
+ return _Database;
56
+ },
57
+ };
58
+ }
48
59
 
49
- async function getQueries() {
50
- if (!_queries) {
51
- _queries = await import('../domain/queries.js');
60
+ async function resolveDbPath(customDbPath, args, allowedRepos) {
61
+ let dbPath = customDbPath || undefined;
62
+ if (args.repo) {
63
+ if (allowedRepos && !allowedRepos.includes(args.repo)) {
64
+ throw new ConfigError(`Repository "${args.repo}" is not in the allowed repos list.`);
52
65
  }
53
- return _queries;
66
+ const { resolveRepoDbPath } = await import('../infrastructure/registry.js');
67
+ const resolved = resolveRepoDbPath(args.repo);
68
+ if (!resolved)
69
+ throw new ConfigError(
70
+ `Repository "${args.repo}" not found in registry or its database is missing.`,
71
+ );
72
+ dbPath = resolved;
54
73
  }
74
+ return dbPath;
75
+ }
55
76
 
56
- function getDatabase() {
57
- if (!_Database) {
58
- const require = createRequire(import.meta.url);
59
- _Database = require('better-sqlite3');
60
- }
61
- return _Database;
77
+ function validateMultiRepoAccess(multiRepo, name, args) {
78
+ if (!multiRepo && args.repo) {
79
+ throw new ConfigError(
80
+ 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
81
+ );
82
+ }
83
+ if (!multiRepo && name === 'list_repos') {
84
+ throw new ConfigError(
85
+ 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to list repositories.',
86
+ );
62
87
  }
88
+ }
89
+
90
+ export async function startMCPServer(customDbPath, options = {}) {
91
+ const { allowedRepos } = options;
92
+ const multiRepo = options.multiRepo || !!allowedRepos;
93
+
94
+ const { Server, StdioServerTransport, ListToolsRequestSchema, CallToolRequestSchema } =
95
+ await loadMCPSdk();
96
+
97
+ // Connect transport FIRST so the server can receive the client's
98
+ // `initialize` request while heavy modules (queries, better-sqlite3)
99
+ // are still loading. These are lazy-loaded on the first tool call
100
+ // and cached for subsequent calls.
101
+ const { getQueries, getDatabase } = createLazyLoaders();
63
102
 
64
103
  const server = new Server(
65
104
  { name: 'codegraph', version: '1.0.0' },
@@ -73,47 +112,17 @@ export async function startMCPServer(customDbPath, options = {}) {
73
112
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
74
113
  const { name, arguments: args } = request.params;
75
114
  try {
76
- if (!multiRepo && args.repo) {
77
- throw new ConfigError(
78
- 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
79
- );
80
- }
81
- if (!multiRepo && name === 'list_repos') {
82
- throw new ConfigError(
83
- 'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to list repositories.',
84
- );
85
- }
86
-
87
- let dbPath = customDbPath || undefined;
88
- if (args.repo) {
89
- if (allowedRepos && !allowedRepos.includes(args.repo)) {
90
- throw new ConfigError(`Repository "${args.repo}" is not in the allowed repos list.`);
91
- }
92
- const { resolveRepoDbPath } = await import('../infrastructure/registry.js');
93
- const resolved = resolveRepoDbPath(args.repo);
94
- if (!resolved)
95
- throw new ConfigError(
96
- `Repository "${args.repo}" not found in registry or its database is missing.`,
97
- );
98
- dbPath = resolved;
99
- }
115
+ validateMultiRepoAccess(multiRepo, name, args);
116
+ const dbPath = await resolveDbPath(customDbPath, args, allowedRepos);
100
117
 
101
118
  const toolEntry = TOOL_HANDLERS.get(name);
102
119
  if (!toolEntry) {
103
120
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
104
121
  }
105
122
 
106
- const ctx = {
107
- dbPath,
108
- getQueries,
109
- getDatabase,
110
- findDbPath,
111
- allowedRepos,
112
- MCP_MAX_LIMIT,
113
- };
114
-
123
+ const ctx = { dbPath, getQueries, getDatabase, findDbPath, allowedRepos, MCP_MAX_LIMIT };
115
124
  const result = await toolEntry.handler(args, ctx);
116
- if (result?.content) return result; // pass-through MCP responses
125
+ if (result?.content) return result;
117
126
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
118
127
  } catch (err) {
119
128
  const code = err instanceof CodegraphError ? err.code : 'UNKNOWN_ERROR';
@@ -99,6 +99,19 @@ const BASE_TOOLS = [
99
99
  required: ['file'],
100
100
  },
101
101
  },
102
+ {
103
+ name: 'brief',
104
+ description:
105
+ 'Token-efficient file summary: symbols with roles and transitive caller counts, importer counts, and file risk tier (high/medium/low). Designed for context injection.',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ file: { type: 'string', description: 'File path (partial match supported)' },
110
+ no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
111
+ },
112
+ required: ['file'],
113
+ },
114
+ },
102
115
  {
103
116
  name: 'file_exports',
104
117
  description:
@@ -0,0 +1,8 @@
1
+ export const name = 'brief';
2
+
3
+ export async function handler(args, ctx) {
4
+ const { briefData } = await ctx.getQueries();
5
+ return briefData(args.file, ctx.dbPath, {
6
+ noTests: args.no_tests,
7
+ });
8
+ }
@@ -6,6 +6,7 @@ import * as astQuery from './ast-query.js';
6
6
  import * as audit from './audit.js';
7
7
  import * as batchQuery from './batch-query.js';
8
8
  import * as branchCompare from './branch-compare.js';
9
+ import * as brief from './brief.js';
9
10
  import * as cfg from './cfg.js';
10
11
  import * as check from './check.js';
11
12
  import * as coChanges from './co-changes.js';
@@ -67,5 +68,6 @@ export const TOOL_HANDLERS = new Map([
67
68
  [dataflow.name, dataflow],
68
69
  [check.name, check],
69
70
  [astQuery.name, astQuery],
71
+ [brief.name, brief],
70
72
  [listRepos.name, listRepos],
71
73
  ]);
@@ -0,0 +1,51 @@
1
+ import { briefData } from '../domain/analysis/brief.js';
2
+ import { outputResult } from './result-formatter.js';
3
+
4
+ /**
5
+ * Format a compact brief for hook context injection.
6
+ * Single-block, token-efficient output.
7
+ *
8
+ * Example:
9
+ * src/domain/graph/builder.js [HIGH RISK]
10
+ * Symbols: buildGraph [core, 12 callers], collectFiles [leaf, 2 callers]
11
+ * Imports: src/db/index.js, src/domain/parser.js
12
+ * Imported by: src/cli/commands/build.js (+8 transitive)
13
+ */
14
+ export function brief(file, customDbPath, opts = {}) {
15
+ const data = briefData(file, customDbPath, opts);
16
+ if (outputResult(data, 'results', opts)) return;
17
+
18
+ if (data.results.length === 0) {
19
+ console.log(`No file matching "${file}" in graph`);
20
+ return;
21
+ }
22
+
23
+ for (const r of data.results) {
24
+ console.log(`${r.file} [${r.risk.toUpperCase()} RISK]`);
25
+
26
+ // Symbols line
27
+ if (r.symbols.length > 0) {
28
+ const parts = r.symbols.map((s) => {
29
+ const tags = [];
30
+ if (s.role) tags.push(s.role);
31
+ tags.push(`${s.callerCount} caller${s.callerCount !== 1 ? 's' : ''}`);
32
+ return `${s.name} [${tags.join(', ')}]`;
33
+ });
34
+ console.log(` Symbols: ${parts.join(', ')}`);
35
+ }
36
+
37
+ // Imports line
38
+ if (r.imports.length > 0) {
39
+ console.log(` Imports: ${r.imports.join(', ')}`);
40
+ }
41
+
42
+ // Imported by line with transitive count
43
+ if (r.importedBy.length > 0) {
44
+ const transitive = r.totalImporterCount - r.importedBy.length;
45
+ const suffix = transitive > 0 ? ` (+${transitive} transitive)` : '';
46
+ console.log(` Imported by: ${r.importedBy.join(', ')}${suffix}`);
47
+ } else if (r.totalImporterCount > 0) {
48
+ console.log(` Imported by: ${r.totalImporterCount} transitive importers`);
49
+ }
50
+ }
51
+ }
@@ -1,19 +1,7 @@
1
1
  import { exportsData, kindIcon } from '../../domain/queries.js';
2
2
  import { outputResult } from '../../infrastructure/result-formatter.js';
3
3
 
4
- export function fileExports(file, customDbPath, opts = {}) {
5
- const data = exportsData(file, customDbPath, opts);
6
- if (outputResult(data, 'results', opts)) return;
7
-
8
- if (data.results.length === 0) {
9
- if (opts.unused) {
10
- console.log(`No unused exports found for "${file}".`);
11
- } else {
12
- console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
13
- }
14
- return;
15
- }
16
-
4
+ function printExportHeader(data, opts) {
17
5
  if (opts.unused) {
18
6
  console.log(
19
7
  `\n# ${data.file} — ${data.totalUnused} unused export${data.totalUnused !== 1 ? 's' : ''} (of ${data.totalExported} exported)\n`,
@@ -24,8 +12,10 @@ export function fileExports(file, customDbPath, opts = {}) {
24
12
  `\n# ${data.file} — ${data.totalExported} exported${unusedNote}, ${data.totalInternal} internal\n`,
25
13
  );
26
14
  }
15
+ }
27
16
 
28
- for (const sym of data.results) {
17
+ function printExportSymbols(results) {
18
+ for (const sym of results) {
29
19
  const icon = kindIcon(sym.kind);
30
20
  const sig = sym.signature?.params ? `(${sym.signature.params})` : '';
31
21
  const role = sym.role ? ` [${sym.role}]` : '';
@@ -38,6 +28,23 @@ export function fileExports(file, customDbPath, opts = {}) {
38
28
  }
39
29
  }
40
30
  }
31
+ }
32
+
33
+ export function fileExports(file, customDbPath, opts = {}) {
34
+ const data = exportsData(file, customDbPath, opts);
35
+ if (outputResult(data, 'results', opts)) return;
36
+
37
+ if (data.results.length === 0) {
38
+ if (opts.unused) {
39
+ console.log(`No unused exports found for "${file}".`);
40
+ } else {
41
+ console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
42
+ }
43
+ return;
44
+ }
45
+
46
+ printExportHeader(data, opts);
47
+ printExportSymbols(data.results);
41
48
 
42
49
  if (data.reexports.length > 0) {
43
50
  console.log(`\n Re-exports: ${data.reexports.map((r) => r.file).join(', ')}`);
@@ -132,6 +132,56 @@ export function fnImpact(name, customDbPath, opts = {}) {
132
132
  }
133
133
  }
134
134
 
135
+ function printDiffFunctions(data) {
136
+ console.log(`\ndiff-impact: ${data.changedFiles} files changed\n`);
137
+ console.log(` ${data.affectedFunctions.length} functions changed:\n`);
138
+ for (const fn of data.affectedFunctions) {
139
+ console.log(` ${kindIcon(fn.kind)} ${fn.name} -- ${fn.file}:${fn.line}`);
140
+ if (fn.transitiveCallers > 0) console.log(` ^ ${fn.transitiveCallers} transitive callers`);
141
+ }
142
+ }
143
+
144
+ function printDiffCoupled(data) {
145
+ if (!data.historicallyCoupled?.length) return;
146
+ console.log('\n Historically coupled (not in static graph):\n');
147
+ for (const c of data.historicallyCoupled) {
148
+ const pct = `${(c.jaccard * 100).toFixed(0)}%`;
149
+ console.log(
150
+ ` ${c.file} <- coupled with ${c.coupledWith} (${pct}, ${c.commitCount} commits)`,
151
+ );
152
+ }
153
+ }
154
+
155
+ function printDiffOwnership(data) {
156
+ if (!data.ownership) return;
157
+ console.log(`\n Affected owners: ${data.ownership.affectedOwners.join(', ')}`);
158
+ console.log(` Suggested reviewers: ${data.ownership.suggestedReviewers.join(', ')}`);
159
+ }
160
+
161
+ function printDiffBoundaries(data) {
162
+ if (!data.boundaryViolations?.length) return;
163
+ console.log(`\n Boundary violations (${data.boundaryViolationCount}):\n`);
164
+ for (const v of data.boundaryViolations) {
165
+ console.log(` [${v.name}] ${v.file} -> ${v.targetFile}`);
166
+ if (v.message) console.log(` ${v.message}`);
167
+ }
168
+ }
169
+
170
+ function printDiffSummary(summary) {
171
+ if (!summary) return;
172
+ let line = `\n Summary: ${summary.functionsChanged} functions changed -> ${summary.callersAffected} callers affected across ${summary.filesAffected} files`;
173
+ if (summary.historicallyCoupledCount > 0) {
174
+ line += `, ${summary.historicallyCoupledCount} historically coupled`;
175
+ }
176
+ if (summary.ownersAffected > 0) {
177
+ line += `, ${summary.ownersAffected} owners affected`;
178
+ }
179
+ if (summary.boundaryViolationCount > 0) {
180
+ line += `, ${summary.boundaryViolationCount} boundary violations`;
181
+ }
182
+ console.log(`${line}\n`);
183
+ }
184
+
135
185
  export function diffImpact(customDbPath, opts = {}) {
136
186
  if (opts.format === 'mermaid') {
137
187
  console.log(diffImpactMermaid(customDbPath, opts));
@@ -156,43 +206,9 @@ export function diffImpact(customDbPath, opts = {}) {
156
206
  return;
157
207
  }
158
208
 
159
- console.log(`\ndiff-impact: ${data.changedFiles} files changed\n`);
160
- console.log(` ${data.affectedFunctions.length} functions changed:\n`);
161
- for (const fn of data.affectedFunctions) {
162
- console.log(` ${kindIcon(fn.kind)} ${fn.name} -- ${fn.file}:${fn.line}`);
163
- if (fn.transitiveCallers > 0) console.log(` ^ ${fn.transitiveCallers} transitive callers`);
164
- }
165
- if (data.historicallyCoupled && data.historicallyCoupled.length > 0) {
166
- console.log('\n Historically coupled (not in static graph):\n');
167
- for (const c of data.historicallyCoupled) {
168
- const pct = `${(c.jaccard * 100).toFixed(0)}%`;
169
- console.log(
170
- ` ${c.file} <- coupled with ${c.coupledWith} (${pct}, ${c.commitCount} commits)`,
171
- );
172
- }
173
- }
174
- if (data.ownership) {
175
- console.log(`\n Affected owners: ${data.ownership.affectedOwners.join(', ')}`);
176
- console.log(` Suggested reviewers: ${data.ownership.suggestedReviewers.join(', ')}`);
177
- }
178
- if (data.boundaryViolations && data.boundaryViolations.length > 0) {
179
- console.log(`\n Boundary violations (${data.boundaryViolationCount}):\n`);
180
- for (const v of data.boundaryViolations) {
181
- console.log(` [${v.name}] ${v.file} -> ${v.targetFile}`);
182
- if (v.message) console.log(` ${v.message}`);
183
- }
184
- }
185
- if (data.summary) {
186
- let summaryLine = `\n Summary: ${data.summary.functionsChanged} functions changed -> ${data.summary.callersAffected} callers affected across ${data.summary.filesAffected} files`;
187
- if (data.summary.historicallyCoupledCount > 0) {
188
- summaryLine += `, ${data.summary.historicallyCoupledCount} historically coupled`;
189
- }
190
- if (data.summary.ownersAffected > 0) {
191
- summaryLine += `, ${data.summary.ownersAffected} owners affected`;
192
- }
193
- if (data.summary.boundaryViolationCount > 0) {
194
- summaryLine += `, ${data.summary.boundaryViolationCount} boundary violations`;
195
- }
196
- console.log(`${summaryLine}\n`);
197
- }
209
+ printDiffFunctions(data);
210
+ printDiffCoupled(data);
211
+ printDiffOwnership(data);
212
+ printDiffBoundaries(data);
213
+ printDiffSummary(data.summary);
198
214
  }