@optave/codegraph 3.1.0 → 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 (83) hide show
  1. package/README.md +5 -5
  2. package/grammars/tree-sitter-go.wasm +0 -0
  3. package/package.json +8 -9
  4. package/src/ast-analysis/engine.js +365 -0
  5. package/src/ast-analysis/metrics.js +118 -0
  6. package/src/ast-analysis/rules/csharp.js +201 -0
  7. package/src/ast-analysis/rules/go.js +182 -0
  8. package/src/ast-analysis/rules/index.js +82 -0
  9. package/src/ast-analysis/rules/java.js +175 -0
  10. package/src/ast-analysis/rules/javascript.js +246 -0
  11. package/src/ast-analysis/rules/php.js +219 -0
  12. package/src/ast-analysis/rules/python.js +196 -0
  13. package/src/ast-analysis/rules/ruby.js +204 -0
  14. package/src/ast-analysis/rules/rust.js +173 -0
  15. package/src/ast-analysis/shared.js +223 -0
  16. package/src/ast-analysis/visitor-utils.js +176 -0
  17. package/src/ast-analysis/visitor.js +162 -0
  18. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  19. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  20. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  21. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  22. package/src/ast.js +26 -166
  23. package/src/audit.js +2 -88
  24. package/src/batch.js +0 -25
  25. package/src/boundaries.js +1 -1
  26. package/src/branch-compare.js +82 -172
  27. package/src/builder.js +48 -184
  28. package/src/cfg.js +148 -1174
  29. package/src/check.js +1 -84
  30. package/src/cli.js +118 -197
  31. package/src/cochange.js +1 -39
  32. package/src/commands/audit.js +88 -0
  33. package/src/commands/batch.js +26 -0
  34. package/src/commands/branch-compare.js +97 -0
  35. package/src/commands/cfg.js +55 -0
  36. package/src/commands/check.js +82 -0
  37. package/src/commands/cochange.js +37 -0
  38. package/src/commands/communities.js +69 -0
  39. package/src/commands/complexity.js +77 -0
  40. package/src/commands/dataflow.js +110 -0
  41. package/src/commands/flow.js +70 -0
  42. package/src/commands/manifesto.js +77 -0
  43. package/src/commands/owners.js +52 -0
  44. package/src/commands/query.js +21 -0
  45. package/src/commands/sequence.js +33 -0
  46. package/src/commands/structure.js +64 -0
  47. package/src/commands/triage.js +49 -0
  48. package/src/communities.js +22 -96
  49. package/src/complexity.js +234 -1591
  50. package/src/cycles.js +1 -1
  51. package/src/dataflow.js +274 -1352
  52. package/src/db/connection.js +88 -0
  53. package/src/db/migrations.js +312 -0
  54. package/src/db/query-builder.js +280 -0
  55. package/src/db/repository/build-stmts.js +104 -0
  56. package/src/db/repository/cfg.js +83 -0
  57. package/src/db/repository/cochange.js +41 -0
  58. package/src/db/repository/complexity.js +15 -0
  59. package/src/db/repository/dataflow.js +12 -0
  60. package/src/db/repository/edges.js +259 -0
  61. package/src/db/repository/embeddings.js +40 -0
  62. package/src/db/repository/graph-read.js +39 -0
  63. package/src/db/repository/index.js +42 -0
  64. package/src/db/repository/nodes.js +236 -0
  65. package/src/db.js +58 -399
  66. package/src/embedder.js +158 -174
  67. package/src/export.js +1 -1
  68. package/src/extractors/javascript.js +130 -5
  69. package/src/flow.js +153 -222
  70. package/src/index.js +53 -16
  71. package/src/infrastructure/result-formatter.js +21 -0
  72. package/src/infrastructure/test-filter.js +7 -0
  73. package/src/kinds.js +50 -0
  74. package/src/manifesto.js +1 -82
  75. package/src/mcp.js +37 -20
  76. package/src/owners.js +127 -182
  77. package/src/queries-cli.js +866 -0
  78. package/src/queries.js +1271 -2416
  79. package/src/sequence.js +179 -223
  80. package/src/structure.js +211 -269
  81. package/src/triage.js +117 -212
  82. package/src/viewer.js +1 -1
  83. package/src/watcher.js +7 -4
package/src/manifesto.js CHANGED
@@ -3,7 +3,7 @@ import { loadConfig } from './config.js';
3
3
  import { findCycles } from './cycles.js';
4
4
  import { openReadonlyOrFail } from './db.js';
5
5
  import { debug } from './logger.js';
6
- import { paginateResult, printNdjson } from './paginate.js';
6
+ import { paginateResult } from './paginate.js';
7
7
 
8
8
  // ─── Rule Definitions ─────────────────────────────────────────────────
9
9
 
@@ -427,84 +427,3 @@ export function manifestoData(customDbPath, opts = {}) {
427
427
  db.close();
428
428
  }
429
429
  }
430
-
431
- /**
432
- * CLI formatter — prints manifesto results and exits with code 1 on failure.
433
- */
434
- export function manifesto(customDbPath, opts = {}) {
435
- const data = manifestoData(customDbPath, opts);
436
-
437
- if (opts.ndjson) {
438
- printNdjson(data, 'violations');
439
- if (!data.passed) process.exit(1);
440
- return;
441
- }
442
- if (opts.json) {
443
- console.log(JSON.stringify(data, null, 2));
444
- if (!data.passed) process.exit(1);
445
- return;
446
- }
447
-
448
- console.log('\n# Manifesto Rules\n');
449
-
450
- // Rules table
451
- console.log(
452
- ` ${'Rule'.padEnd(20)} ${'Level'.padEnd(10)} ${'Status'.padEnd(8)} ${'Warn'.padStart(6)} ${'Fail'.padStart(6)} ${'Violations'.padStart(11)}`,
453
- );
454
- console.log(
455
- ` ${'─'.repeat(20)} ${'─'.repeat(10)} ${'─'.repeat(8)} ${'─'.repeat(6)} ${'─'.repeat(6)} ${'─'.repeat(11)}`,
456
- );
457
-
458
- for (const rule of data.rules) {
459
- const warn = rule.thresholds.warn != null ? String(rule.thresholds.warn) : '—';
460
- const fail = rule.thresholds.fail != null ? String(rule.thresholds.fail) : '—';
461
- const statusIcon = rule.status === 'pass' ? 'pass' : rule.status === 'warn' ? 'WARN' : 'FAIL';
462
- console.log(
463
- ` ${rule.name.padEnd(20)} ${rule.level.padEnd(10)} ${statusIcon.padEnd(8)} ${warn.padStart(6)} ${fail.padStart(6)} ${String(rule.violationCount).padStart(11)}`,
464
- );
465
- }
466
-
467
- // Summary
468
- const s = data.summary;
469
- console.log(
470
- `\n ${s.total} rules | ${s.passed} passed | ${s.warned} warned | ${s.failed} failed | ${s.violationCount} violations`,
471
- );
472
-
473
- // Violations detail
474
- if (data.violations.length > 0) {
475
- const failViolations = data.violations.filter((v) => v.level === 'fail');
476
- const warnViolations = data.violations.filter((v) => v.level === 'warn');
477
-
478
- if (failViolations.length > 0) {
479
- console.log(`\n## Failures (${failViolations.length})\n`);
480
- for (const v of failViolations.slice(0, 20)) {
481
- const loc = v.line ? `${v.file}:${v.line}` : v.file;
482
- console.log(
483
- ` [FAIL] ${v.rule}: ${v.name} (${v.value}) at ${loc} — threshold ${v.threshold}`,
484
- );
485
- }
486
- if (failViolations.length > 20) {
487
- console.log(` ... and ${failViolations.length - 20} more`);
488
- }
489
- }
490
-
491
- if (warnViolations.length > 0) {
492
- console.log(`\n## Warnings (${warnViolations.length})\n`);
493
- for (const v of warnViolations.slice(0, 20)) {
494
- const loc = v.line ? `${v.file}:${v.line}` : v.file;
495
- console.log(
496
- ` [WARN] ${v.rule}: ${v.name} (${v.value}) at ${loc} — threshold ${v.threshold}`,
497
- );
498
- }
499
- if (warnViolations.length > 20) {
500
- console.log(` ... and ${warnViolations.length - 20} more`);
501
- }
502
- }
503
- }
504
-
505
- console.log();
506
-
507
- if (!data.passed) {
508
- process.exit(1);
509
- }
510
- }
package/src/mcp.js CHANGED
@@ -836,26 +836,27 @@ export async function startMCPServer(customDbPath, options = {}) {
836
836
  process.exit(1);
837
837
  }
838
838
 
839
- // Lazy import query functions to avoid circular deps at module load
840
- const {
841
- impactAnalysisData,
842
- moduleMapData,
843
- fileDepsData,
844
- exportsData,
845
- fnDepsData,
846
- fnImpactData,
847
- pathData,
848
- contextData,
849
- childrenData,
850
- explainData,
851
- whereData,
852
- diffImpactData,
853
- listFunctionsData,
854
- rolesData,
855
- } = await import('./queries.js');
839
+ // Connect transport FIRST so the server can receive the client's
840
+ // `initialize` request while heavy modules (queries, better-sqlite3)
841
+ // are still loading. These are lazy-loaded on the first tool call
842
+ // and cached for subsequent calls.
843
+ let _queries;
844
+ let _Database;
856
845
 
857
- const require = createRequire(import.meta.url);
858
- const Database = require('better-sqlite3');
846
+ async function getQueries() {
847
+ if (!_queries) {
848
+ _queries = await import('./queries.js');
849
+ }
850
+ return _queries;
851
+ }
852
+
853
+ function getDatabase() {
854
+ if (!_Database) {
855
+ const require = createRequire(import.meta.url);
856
+ _Database = require('better-sqlite3');
857
+ }
858
+ return _Database;
859
+ }
859
860
 
860
861
  const server = new Server(
861
862
  { name: 'codegraph', version: '1.0.0' },
@@ -868,8 +869,24 @@ export async function startMCPServer(customDbPath, options = {}) {
868
869
 
869
870
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
870
871
  const { name, arguments: args } = request.params;
871
-
872
872
  try {
873
+ const {
874
+ impactAnalysisData,
875
+ moduleMapData,
876
+ fileDepsData,
877
+ exportsData,
878
+ fnDepsData,
879
+ fnImpactData,
880
+ pathData,
881
+ contextData,
882
+ childrenData,
883
+ explainData,
884
+ whereData,
885
+ diffImpactData,
886
+ listFunctionsData,
887
+ rolesData,
888
+ } = await getQueries();
889
+ const Database = getDatabase();
873
890
  if (!multiRepo && args.repo) {
874
891
  throw new Error(
875
892
  'Multi-repo access is disabled. Restart with `codegraph mcp --multi-repo` to access other repositories.',
package/src/owners.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { findDbPath, openReadonlyOrFail } from './db.js';
4
- import { isTestFile } from './queries.js';
4
+ import { isTestFile } from './infrastructure/test-filter.js';
5
5
 
6
6
  // ─── CODEOWNERS Parsing ──────────────────────────────────────────────
7
7
 
@@ -163,197 +163,142 @@ export function ownersForFiles(filePaths, repoRoot) {
163
163
  */
164
164
  export function ownersData(customDbPath, opts = {}) {
165
165
  const db = openReadonlyOrFail(customDbPath);
166
- const dbPath = findDbPath(customDbPath);
167
- const repoRoot = path.resolve(path.dirname(dbPath), '..');
168
-
169
- const parsed = parseCodeowners(repoRoot);
170
- if (!parsed) {
171
- db.close();
172
- return {
173
- codeownersFile: null,
174
- files: [],
175
- symbols: [],
176
- boundaries: [],
177
- summary: {
178
- totalFiles: 0,
179
- ownedFiles: 0,
180
- unownedFiles: 0,
181
- coveragePercent: 0,
182
- ownerCount: 0,
183
- byOwner: [],
184
- },
185
- };
186
- }
187
-
188
- // Get all distinct files from nodes
189
- let allFiles = db
190
- .prepare('SELECT DISTINCT file FROM nodes')
191
- .all()
192
- .map((r) => r.file);
166
+ try {
167
+ const dbPath = findDbPath(customDbPath);
168
+ const repoRoot = path.resolve(path.dirname(dbPath), '..');
169
+
170
+ const parsed = parseCodeowners(repoRoot);
171
+ if (!parsed) {
172
+ return {
173
+ codeownersFile: null,
174
+ files: [],
175
+ symbols: [],
176
+ boundaries: [],
177
+ summary: {
178
+ totalFiles: 0,
179
+ ownedFiles: 0,
180
+ unownedFiles: 0,
181
+ coveragePercent: 0,
182
+ ownerCount: 0,
183
+ byOwner: [],
184
+ },
185
+ };
186
+ }
193
187
 
194
- if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
195
- if (opts.file) {
196
- const filter = opts.file;
197
- allFiles = allFiles.filter((f) => f.includes(filter));
198
- }
188
+ // Get all distinct files from nodes
189
+ let allFiles = db
190
+ .prepare('SELECT DISTINCT file FROM nodes')
191
+ .all()
192
+ .map((r) => r.file);
199
193
 
200
- // Map files to owners
201
- const fileOwners = allFiles.map((file) => ({
202
- file,
203
- owners: matchOwners(file, parsed.rules),
204
- }));
205
-
206
- // Build owner-to-files index
207
- const ownerIndex = new Map();
208
- let ownedCount = 0;
209
- for (const fo of fileOwners) {
210
- if (fo.owners.length > 0) ownedCount++;
211
- for (const o of fo.owners) {
212
- if (!ownerIndex.has(o)) ownerIndex.set(o, []);
213
- ownerIndex.get(o).push(fo.file);
194
+ if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
195
+ if (opts.file) {
196
+ const filter = opts.file;
197
+ allFiles = allFiles.filter((f) => f.includes(filter));
214
198
  }
215
- }
216
199
 
217
- // Filter files if --owner specified
218
- let filteredFiles = fileOwners;
219
- if (opts.owner) {
220
- filteredFiles = fileOwners.filter((fo) => fo.owners.includes(opts.owner));
221
- }
222
-
223
- // Get symbols for filtered files
224
- const fileSet = new Set(filteredFiles.map((fo) => fo.file));
225
- let symbols = db
226
- .prepare('SELECT name, kind, file, line FROM nodes')
227
- .all()
228
- .filter((n) => fileSet.has(n.file));
229
-
230
- if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
231
- if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
232
-
233
- const symbolsWithOwners = symbols.map((s) => ({
234
- ...s,
235
- owners: matchOwners(s.file, parsed.rules),
236
- }));
237
-
238
- // Boundary analysis — cross-owner call edges
239
- const boundaries = [];
240
- if (opts.boundary) {
241
- const edges = db
242
- .prepare(
243
- `SELECT e.id, e.kind AS edgeKind,
244
- s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
245
- t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
246
- FROM edges e
247
- JOIN nodes s ON e.source_id = s.id
248
- JOIN nodes t ON e.target_id = t.id
249
- WHERE e.kind = 'calls'`,
250
- )
251
- .all();
252
-
253
- for (const e of edges) {
254
- if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
255
- const srcOwners = matchOwners(e.srcFile, parsed.rules);
256
- const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
257
- // Cross-boundary: different owner sets
258
- const srcKey = srcOwners.sort().join(',');
259
- const tgtKey = tgtOwners.sort().join(',');
260
- if (srcKey !== tgtKey) {
261
- boundaries.push({
262
- from: {
263
- name: e.srcName,
264
- kind: e.srcKind,
265
- file: e.srcFile,
266
- line: e.srcLine,
267
- owners: srcOwners,
268
- },
269
- to: {
270
- name: e.tgtName,
271
- kind: e.tgtKind,
272
- file: e.tgtFile,
273
- line: e.tgtLine,
274
- owners: tgtOwners,
275
- },
276
- edgeKind: e.edgeKind,
277
- });
200
+ // Map files to owners
201
+ const fileOwners = allFiles.map((file) => ({
202
+ file,
203
+ owners: matchOwners(file, parsed.rules),
204
+ }));
205
+
206
+ // Build owner-to-files index
207
+ const ownerIndex = new Map();
208
+ let ownedCount = 0;
209
+ for (const fo of fileOwners) {
210
+ if (fo.owners.length > 0) ownedCount++;
211
+ for (const o of fo.owners) {
212
+ if (!ownerIndex.has(o)) ownerIndex.set(o, []);
213
+ ownerIndex.get(o).push(fo.file);
278
214
  }
279
215
  }
280
- }
281
216
 
282
- // Summary
283
- const byOwner = [...ownerIndex.entries()]
284
- .map(([owner, files]) => ({ owner, fileCount: files.length }))
285
- .sort((a, b) => b.fileCount - a.fileCount);
286
-
287
- db.close();
288
- return {
289
- codeownersFile: parsed.path,
290
- files: filteredFiles,
291
- symbols: symbolsWithOwners,
292
- boundaries,
293
- summary: {
294
- totalFiles: allFiles.length,
295
- ownedFiles: ownedCount,
296
- unownedFiles: allFiles.length - ownedCount,
297
- coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
298
- ownerCount: ownerIndex.size,
299
- byOwner,
300
- },
301
- };
302
- }
303
-
304
- // ─── CLI Display ─────────────────────────────────────────────────────
305
-
306
- /**
307
- * CLI display function for the `owners` command.
308
- * @param {string} [customDbPath]
309
- * @param {object} [opts]
310
- */
311
- export function owners(customDbPath, opts = {}) {
312
- const data = ownersData(customDbPath, opts);
313
- if (opts.json) {
314
- console.log(JSON.stringify(data, null, 2));
315
- return;
316
- }
317
-
318
- if (!data.codeownersFile) {
319
- console.log('No CODEOWNERS file found.');
320
- return;
321
- }
322
-
323
- console.log(`\nCODEOWNERS: ${data.codeownersFile}\n`);
324
-
325
- const s = data.summary;
326
- console.log(
327
- ` Coverage: ${s.coveragePercent}% (${s.ownedFiles}/${s.totalFiles} files owned, ${s.ownerCount} owners)\n`,
328
- );
329
-
330
- if (s.byOwner.length > 0) {
331
- console.log(' Owners:\n');
332
- for (const o of s.byOwner) {
333
- console.log(` ${o.owner} ${o.fileCount} files`);
217
+ // Filter files if --owner specified
218
+ let filteredFiles = fileOwners;
219
+ if (opts.owner) {
220
+ filteredFiles = fileOwners.filter((fo) => fo.owners.includes(opts.owner));
334
221
  }
335
- console.log();
336
- }
337
222
 
338
- if (data.files.length > 0 && opts.owner) {
339
- console.log(` Files owned by ${opts.owner}:\n`);
340
- for (const f of data.files) {
341
- console.log(` ${f.file}`);
223
+ // Get symbols for filtered files
224
+ const fileSet = new Set(filteredFiles.map((fo) => fo.file));
225
+ let symbols = db
226
+ .prepare('SELECT name, kind, file, line FROM nodes')
227
+ .all()
228
+ .filter((n) => fileSet.has(n.file));
229
+
230
+ if (opts.noTests) symbols = symbols.filter((s) => !isTestFile(s.file));
231
+ if (opts.kind) symbols = symbols.filter((s) => s.kind === opts.kind);
232
+
233
+ const symbolsWithOwners = symbols.map((s) => ({
234
+ ...s,
235
+ owners: matchOwners(s.file, parsed.rules),
236
+ }));
237
+
238
+ // Boundary analysis — cross-owner call edges
239
+ const boundaries = [];
240
+ if (opts.boundary) {
241
+ const edges = db
242
+ .prepare(
243
+ `SELECT e.id, e.kind AS edgeKind,
244
+ s.name AS srcName, s.kind AS srcKind, s.file AS srcFile, s.line AS srcLine,
245
+ t.name AS tgtName, t.kind AS tgtKind, t.file AS tgtFile, t.line AS tgtLine
246
+ FROM edges e
247
+ JOIN nodes s ON e.source_id = s.id
248
+ JOIN nodes t ON e.target_id = t.id
249
+ WHERE e.kind = 'calls'`,
250
+ )
251
+ .all();
252
+
253
+ for (const e of edges) {
254
+ if (opts.noTests && (isTestFile(e.srcFile) || isTestFile(e.tgtFile))) continue;
255
+ const srcOwners = matchOwners(e.srcFile, parsed.rules);
256
+ const tgtOwners = matchOwners(e.tgtFile, parsed.rules);
257
+ // Cross-boundary: different owner sets
258
+ const srcKey = srcOwners.sort().join(',');
259
+ const tgtKey = tgtOwners.sort().join(',');
260
+ if (srcKey !== tgtKey) {
261
+ boundaries.push({
262
+ from: {
263
+ name: e.srcName,
264
+ kind: e.srcKind,
265
+ file: e.srcFile,
266
+ line: e.srcLine,
267
+ owners: srcOwners,
268
+ },
269
+ to: {
270
+ name: e.tgtName,
271
+ kind: e.tgtKind,
272
+ file: e.tgtFile,
273
+ line: e.tgtLine,
274
+ owners: tgtOwners,
275
+ },
276
+ edgeKind: e.edgeKind,
277
+ });
278
+ }
279
+ }
342
280
  }
343
- console.log();
344
- }
345
281
 
346
- if (data.boundaries.length > 0) {
347
- console.log(` Cross-owner boundaries: ${data.boundaries.length} edges\n`);
348
- const shown = data.boundaries.slice(0, 30);
349
- for (const b of shown) {
350
- const srcOwner = b.from.owners.join(', ') || '(unowned)';
351
- const tgtOwner = b.to.owners.join(', ') || '(unowned)';
352
- console.log(` ${b.from.name} [${srcOwner}] -> ${b.to.name} [${tgtOwner}]`);
353
- }
354
- if (data.boundaries.length > 30) {
355
- console.log(` ... and ${data.boundaries.length - 30} more`);
356
- }
357
- console.log();
282
+ // Summary
283
+ const byOwner = [...ownerIndex.entries()]
284
+ .map(([owner, files]) => ({ owner, fileCount: files.length }))
285
+ .sort((a, b) => b.fileCount - a.fileCount);
286
+
287
+ return {
288
+ codeownersFile: parsed.path,
289
+ files: filteredFiles,
290
+ symbols: symbolsWithOwners,
291
+ boundaries,
292
+ summary: {
293
+ totalFiles: allFiles.length,
294
+ ownedFiles: ownedCount,
295
+ unownedFiles: allFiles.length - ownedCount,
296
+ coveragePercent: allFiles.length > 0 ? Math.round((ownedCount / allFiles.length) * 100) : 0,
297
+ ownerCount: ownerIndex.size,
298
+ byOwner,
299
+ },
300
+ };
301
+ } finally {
302
+ db.close();
358
303
  }
359
304
  }