@optave/codegraph 3.1.3 → 3.1.5

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 (232) hide show
  1. package/README.md +38 -84
  2. package/package.json +13 -8
  3. package/src/ast-analysis/engine.js +32 -12
  4. package/src/ast-analysis/shared.js +6 -5
  5. package/src/cli/commands/ast.js +22 -0
  6. package/src/cli/commands/audit.js +45 -0
  7. package/src/cli/commands/batch.js +68 -0
  8. package/src/cli/commands/branch-compare.js +21 -0
  9. package/src/cli/commands/build.js +26 -0
  10. package/src/cli/commands/cfg.js +26 -0
  11. package/src/cli/commands/check.js +74 -0
  12. package/src/cli/commands/children.js +28 -0
  13. package/src/cli/commands/co-change.js +67 -0
  14. package/src/cli/commands/communities.js +19 -0
  15. package/src/cli/commands/complexity.js +46 -0
  16. package/src/cli/commands/context.js +30 -0
  17. package/src/cli/commands/cycles.js +32 -0
  18. package/src/cli/commands/dataflow.js +28 -0
  19. package/src/cli/commands/deps.js +12 -0
  20. package/src/cli/commands/diff-impact.js +26 -0
  21. package/src/cli/commands/embed.js +30 -0
  22. package/src/cli/commands/export.js +78 -0
  23. package/src/cli/commands/exports.js +14 -0
  24. package/src/cli/commands/flow.js +32 -0
  25. package/src/cli/commands/fn-impact.js +26 -0
  26. package/src/cli/commands/impact.js +12 -0
  27. package/src/cli/commands/info.js +76 -0
  28. package/src/cli/commands/map.js +19 -0
  29. package/src/cli/commands/mcp.js +18 -0
  30. package/src/cli/commands/models.js +19 -0
  31. package/src/cli/commands/owners.js +25 -0
  32. package/src/cli/commands/path.js +36 -0
  33. package/src/cli/commands/plot.js +89 -0
  34. package/src/cli/commands/query.js +45 -0
  35. package/src/cli/commands/registry.js +100 -0
  36. package/src/cli/commands/roles.js +30 -0
  37. package/src/cli/commands/search.js +42 -0
  38. package/src/cli/commands/sequence.js +28 -0
  39. package/src/cli/commands/snapshot.js +66 -0
  40. package/src/cli/commands/stats.js +15 -0
  41. package/src/cli/commands/structure.js +33 -0
  42. package/src/cli/commands/triage.js +78 -0
  43. package/src/cli/commands/watch.js +12 -0
  44. package/src/cli/commands/where.js +20 -0
  45. package/src/cli/index.js +124 -0
  46. package/src/cli/shared/open-graph.js +13 -0
  47. package/src/cli/shared/options.js +59 -0
  48. package/src/cli/shared/output.js +1 -0
  49. package/src/cli.js +11 -1522
  50. package/src/db/connection.js +130 -7
  51. package/src/{db.js → db/index.js} +17 -5
  52. package/src/db/migrations.js +42 -1
  53. package/src/db/query-builder.js +20 -12
  54. package/src/db/repository/base.js +201 -0
  55. package/src/db/repository/graph-read.js +7 -4
  56. package/src/db/repository/in-memory-repository.js +575 -0
  57. package/src/db/repository/index.js +5 -1
  58. package/src/db/repository/nodes.js +60 -6
  59. package/src/db/repository/sqlite-repository.js +219 -0
  60. package/src/domain/analysis/context.js +408 -0
  61. package/src/domain/analysis/dependencies.js +341 -0
  62. package/src/domain/analysis/exports.js +134 -0
  63. package/src/domain/analysis/impact.js +466 -0
  64. package/src/domain/analysis/module-map.js +322 -0
  65. package/src/domain/analysis/roles.js +45 -0
  66. package/src/domain/analysis/symbol-lookup.js +238 -0
  67. package/src/domain/graph/builder/context.js +85 -0
  68. package/src/domain/graph/builder/helpers.js +218 -0
  69. package/src/domain/graph/builder/incremental.js +178 -0
  70. package/src/domain/graph/builder/pipeline.js +130 -0
  71. package/src/domain/graph/builder/stages/build-edges.js +297 -0
  72. package/src/domain/graph/builder/stages/build-structure.js +113 -0
  73. package/src/domain/graph/builder/stages/collect-files.js +44 -0
  74. package/src/domain/graph/builder/stages/detect-changes.js +413 -0
  75. package/src/domain/graph/builder/stages/finalize.js +139 -0
  76. package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
  77. package/src/domain/graph/builder/stages/parse-files.js +28 -0
  78. package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
  79. package/src/domain/graph/builder/stages/run-analyses.js +44 -0
  80. package/src/domain/graph/builder.js +11 -0
  81. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  82. package/src/domain/graph/cycles.js +82 -0
  83. package/src/{journal.js → domain/graph/journal.js} +1 -1
  84. package/src/{resolve.js → domain/graph/resolve.js} +3 -3
  85. package/src/{watcher.js → domain/graph/watcher.js} +10 -150
  86. package/src/{parser.js → domain/parser.js} +5 -5
  87. package/src/domain/queries.js +48 -0
  88. package/src/domain/search/generator.js +163 -0
  89. package/src/domain/search/index.js +13 -0
  90. package/src/domain/search/models.js +218 -0
  91. package/src/domain/search/search/cli-formatter.js +151 -0
  92. package/src/domain/search/search/filters.js +46 -0
  93. package/src/domain/search/search/hybrid.js +121 -0
  94. package/src/domain/search/search/keyword.js +68 -0
  95. package/src/domain/search/search/prepare.js +66 -0
  96. package/src/domain/search/search/semantic.js +145 -0
  97. package/src/domain/search/stores/fts5.js +27 -0
  98. package/src/domain/search/stores/sqlite-blob.js +24 -0
  99. package/src/domain/search/strategies/source.js +14 -0
  100. package/src/domain/search/strategies/structured.js +43 -0
  101. package/src/domain/search/strategies/text-utils.js +43 -0
  102. package/src/extractors/csharp.js +10 -2
  103. package/src/extractors/go.js +3 -1
  104. package/src/extractors/helpers.js +71 -0
  105. package/src/extractors/java.js +9 -2
  106. package/src/extractors/javascript.js +39 -2
  107. package/src/extractors/php.js +3 -1
  108. package/src/extractors/python.js +14 -3
  109. package/src/extractors/rust.js +3 -1
  110. package/src/{ast.js → features/ast.js} +8 -8
  111. package/src/{audit.js → features/audit.js} +16 -44
  112. package/src/{batch.js → features/batch.js} +6 -5
  113. package/src/{boundaries.js → features/boundaries.js} +2 -2
  114. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  115. package/src/{cfg.js → features/cfg.js} +11 -12
  116. package/src/{check.js → features/check.js} +13 -30
  117. package/src/{cochange.js → features/cochange.js} +5 -5
  118. package/src/{communities.js → features/communities.js} +18 -90
  119. package/src/{complexity.js → features/complexity.js} +13 -13
  120. package/src/{dataflow.js → features/dataflow.js} +12 -13
  121. package/src/features/export.js +378 -0
  122. package/src/{flow.js → features/flow.js} +4 -4
  123. package/src/features/graph-enrichment.js +327 -0
  124. package/src/{manifesto.js → features/manifesto.js} +6 -6
  125. package/src/{owners.js → features/owners.js} +2 -2
  126. package/src/{sequence.js → features/sequence.js} +16 -52
  127. package/src/{snapshot.js → features/snapshot.js} +8 -7
  128. package/src/{structure.js → features/structure.js} +20 -45
  129. package/src/{triage.js → features/triage.js} +27 -79
  130. package/src/graph/algorithms/bfs.js +49 -0
  131. package/src/graph/algorithms/centrality.js +16 -0
  132. package/src/graph/algorithms/index.js +5 -0
  133. package/src/graph/algorithms/louvain.js +26 -0
  134. package/src/graph/algorithms/shortest-path.js +41 -0
  135. package/src/graph/algorithms/tarjan.js +49 -0
  136. package/src/graph/builders/dependency.js +110 -0
  137. package/src/graph/builders/index.js +3 -0
  138. package/src/graph/builders/structure.js +40 -0
  139. package/src/graph/builders/temporal.js +33 -0
  140. package/src/graph/classifiers/index.js +2 -0
  141. package/src/graph/classifiers/risk.js +85 -0
  142. package/src/graph/classifiers/roles.js +64 -0
  143. package/src/graph/index.js +13 -0
  144. package/src/graph/model.js +230 -0
  145. package/src/index.cjs +16 -0
  146. package/src/index.js +42 -219
  147. package/src/{native.js → infrastructure/native.js} +3 -1
  148. package/src/infrastructure/result-formatter.js +2 -21
  149. package/src/mcp/index.js +2 -0
  150. package/src/mcp/middleware.js +26 -0
  151. package/src/mcp/server.js +128 -0
  152. package/src/{mcp.js → mcp/tool-registry.js} +6 -675
  153. package/src/mcp/tools/ast-query.js +14 -0
  154. package/src/mcp/tools/audit.js +21 -0
  155. package/src/mcp/tools/batch-query.js +11 -0
  156. package/src/mcp/tools/branch-compare.js +12 -0
  157. package/src/mcp/tools/cfg.js +21 -0
  158. package/src/mcp/tools/check.js +43 -0
  159. package/src/mcp/tools/co-changes.js +20 -0
  160. package/src/mcp/tools/code-owners.js +12 -0
  161. package/src/mcp/tools/communities.js +15 -0
  162. package/src/mcp/tools/complexity.js +18 -0
  163. package/src/mcp/tools/context.js +17 -0
  164. package/src/mcp/tools/dataflow.js +26 -0
  165. package/src/mcp/tools/diff-impact.js +24 -0
  166. package/src/mcp/tools/execution-flow.js +26 -0
  167. package/src/mcp/tools/export-graph.js +57 -0
  168. package/src/mcp/tools/file-deps.js +12 -0
  169. package/src/mcp/tools/file-exports.js +13 -0
  170. package/src/mcp/tools/find-cycles.js +15 -0
  171. package/src/mcp/tools/fn-impact.js +15 -0
  172. package/src/mcp/tools/impact-analysis.js +12 -0
  173. package/src/mcp/tools/index.js +71 -0
  174. package/src/mcp/tools/list-functions.js +14 -0
  175. package/src/mcp/tools/list-repos.js +11 -0
  176. package/src/mcp/tools/module-map.js +6 -0
  177. package/src/mcp/tools/node-roles.js +14 -0
  178. package/src/mcp/tools/path.js +12 -0
  179. package/src/mcp/tools/query.js +30 -0
  180. package/src/mcp/tools/semantic-search.js +65 -0
  181. package/src/mcp/tools/sequence.js +17 -0
  182. package/src/mcp/tools/structure.js +15 -0
  183. package/src/mcp/tools/symbol-children.js +14 -0
  184. package/src/mcp/tools/triage.js +35 -0
  185. package/src/mcp/tools/where.js +13 -0
  186. package/src/{commands → presentation}/audit.js +2 -2
  187. package/src/{commands → presentation}/batch.js +1 -1
  188. package/src/{commands → presentation}/branch-compare.js +2 -2
  189. package/src/{commands → presentation}/cfg.js +1 -1
  190. package/src/{commands → presentation}/check.js +6 -6
  191. package/src/presentation/colors.js +44 -0
  192. package/src/{commands → presentation}/communities.js +1 -1
  193. package/src/{commands → presentation}/complexity.js +1 -1
  194. package/src/{commands → presentation}/dataflow.js +1 -1
  195. package/src/presentation/export.js +444 -0
  196. package/src/{commands → presentation}/flow.js +2 -2
  197. package/src/{commands → presentation}/manifesto.js +4 -4
  198. package/src/{commands → presentation}/owners.js +1 -1
  199. package/src/presentation/queries-cli/exports.js +46 -0
  200. package/src/presentation/queries-cli/impact.js +198 -0
  201. package/src/presentation/queries-cli/index.js +5 -0
  202. package/src/presentation/queries-cli/inspect.js +334 -0
  203. package/src/presentation/queries-cli/overview.js +197 -0
  204. package/src/presentation/queries-cli/path.js +58 -0
  205. package/src/presentation/queries-cli.js +27 -0
  206. package/src/{commands → presentation}/query.js +1 -1
  207. package/src/presentation/result-formatter.js +144 -0
  208. package/src/presentation/sequence-renderer.js +43 -0
  209. package/src/{commands → presentation}/sequence.js +2 -2
  210. package/src/{commands → presentation}/structure.js +2 -2
  211. package/src/presentation/table.js +47 -0
  212. package/src/{commands → presentation}/triage.js +1 -1
  213. package/src/{viewer.js → presentation/viewer.js} +68 -382
  214. package/src/{constants.js → shared/constants.js} +1 -1
  215. package/src/shared/errors.js +78 -0
  216. package/src/shared/file-utils.js +153 -0
  217. package/src/shared/generators.js +125 -0
  218. package/src/shared/hierarchy.js +27 -0
  219. package/src/shared/normalize.js +59 -0
  220. package/src/builder.js +0 -1486
  221. package/src/cycles.js +0 -137
  222. package/src/embedder.js +0 -1097
  223. package/src/export.js +0 -681
  224. package/src/queries-cli.js +0 -866
  225. package/src/queries.js +0 -2289
  226. /package/src/{config.js → infrastructure/config.js} +0 -0
  227. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  228. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  229. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  230. /package/src/{commands → presentation}/cochange.js +0 -0
  231. /package/src/{kinds.js → shared/kinds.js} +0 -0
  232. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -0,0 +1,18 @@
1
+ export const command = {
2
+ name: 'mcp',
3
+ description: 'Start MCP (Model Context Protocol) server for AI assistant integration',
4
+ options: [
5
+ ['-d, --db <path>', 'Path to graph.db'],
6
+ ['--multi-repo', 'Enable access to all registered repositories'],
7
+ ['--repos <names>', 'Comma-separated list of allowed repo names (restricts access)'],
8
+ ],
9
+ async execute(_args, opts) {
10
+ const { startMCPServer } = await import('../../mcp/index.js');
11
+ const mcpOpts = {};
12
+ mcpOpts.multiRepo = opts.multiRepo || !!opts.repos;
13
+ if (opts.repos) {
14
+ mcpOpts.allowedRepos = opts.repos.split(',').map((s) => s.trim());
15
+ }
16
+ await startMCPServer(opts.db, mcpOpts);
17
+ },
18
+ };
@@ -0,0 +1,19 @@
1
+ import { DEFAULT_MODEL, MODELS } from '../../domain/search/index.js';
2
+
3
+ export const command = {
4
+ name: 'models',
5
+ description: 'List available embedding models',
6
+ execute(_args, _opts, ctx) {
7
+ const defaultModel = ctx.config.embeddings?.model || DEFAULT_MODEL;
8
+ console.log('\nAvailable embedding models:\n');
9
+ for (const [key, cfg] of Object.entries(MODELS)) {
10
+ const def = key === defaultModel ? ' (default)' : '';
11
+ const ctxWindow = cfg.contextWindow ? `${cfg.contextWindow} ctx` : '';
12
+ console.log(
13
+ ` ${key.padEnd(12)} ${String(cfg.dim).padStart(4)}d ${ctxWindow.padEnd(9)} ${cfg.desc}${def}`,
14
+ );
15
+ }
16
+ console.log('\nUsage: codegraph embed --model <name> --strategy <structured|source>');
17
+ console.log(' codegraph search "query" --model <name>\n');
18
+ },
19
+ };
@@ -0,0 +1,25 @@
1
+ export const command = {
2
+ name: 'owners [target]',
3
+ description: 'Show CODEOWNERS mapping for files and functions',
4
+ options: [
5
+ ['-d, --db <path>', 'Path to graph.db'],
6
+ ['--owner <owner>', 'Filter to a specific owner'],
7
+ ['--boundary', 'Show cross-owner boundary edges'],
8
+ ['-f, --file <path>', 'Scope to a specific file'],
9
+ ['-k, --kind <kind>', 'Filter by symbol kind'],
10
+ ['-T, --no-tests', 'Exclude test/spec files'],
11
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
12
+ ['-j, --json', 'Output as JSON'],
13
+ ],
14
+ async execute([target], opts, ctx) {
15
+ const { owners } = await import('../../presentation/owners.js');
16
+ owners(opts.db, {
17
+ owner: opts.owner,
18
+ boundary: opts.boundary,
19
+ file: opts.file || target,
20
+ kind: opts.kind,
21
+ noTests: ctx.resolveNoTests(opts),
22
+ json: opts.json,
23
+ });
24
+ },
25
+ };
@@ -0,0 +1,36 @@
1
+ import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
2
+ import { symbolPath } from '../../presentation/queries-cli.js';
3
+
4
+ export const command = {
5
+ name: 'path <from> <to>',
6
+ description: 'Find shortest path between two symbols',
7
+ options: [
8
+ ['-d, --db <path>', 'Path to graph.db'],
9
+ ['--reverse', 'Follow edges backward'],
10
+ ['--kinds <kinds>', 'Comma-separated edge kinds to follow (default: calls)'],
11
+ ['--from-file <path>', 'Disambiguate source symbol by file'],
12
+ ['--to-file <path>', 'Disambiguate target symbol by file'],
13
+ ['--depth <n>', 'Max traversal depth', '10'],
14
+ ['-k, --kind <kind>', 'Filter to a specific symbol kind'],
15
+ ['-T, --no-tests', 'Exclude test/spec files from results'],
16
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
17
+ ['-j, --json', 'Output as JSON'],
18
+ ],
19
+ validate([_from, _to], opts) {
20
+ if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
21
+ return `Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`;
22
+ }
23
+ },
24
+ execute([from, to], opts, ctx) {
25
+ symbolPath(from, to, opts.db, {
26
+ maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
27
+ edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
28
+ reverse: opts.reverse,
29
+ fromFile: opts.fromFile,
30
+ toFile: opts.toFile,
31
+ kind: opts.kind,
32
+ noTests: ctx.resolveNoTests(opts),
33
+ json: opts.json,
34
+ });
35
+ },
36
+ };
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { openGraph } from '../shared/open-graph.js';
4
+
5
+ export const command = {
6
+ name: 'plot',
7
+ description: 'Generate an interactive HTML dependency graph viewer',
8
+ options: [
9
+ ['-d, --db <path>', 'Path to graph.db'],
10
+ ['--functions', 'Function-level graph instead of file-level'],
11
+ ['-T, --no-tests', 'Exclude test/spec files'],
12
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
13
+ ['--min-confidence <score>', 'Minimum edge confidence threshold (default: 0.5)', '0.5'],
14
+ ['-o, --output <file>', 'Write HTML to file'],
15
+ ['-c, --config <path>', 'Path to .plotDotCfg config file'],
16
+ ['--no-open', 'Do not open in browser'],
17
+ ['--cluster <mode>', 'Cluster nodes: none | community | directory'],
18
+ ['--overlay <list>', 'Comma-separated overlays: complexity,risk'],
19
+ ['--seed <strategy>', 'Seed strategy: all | top-fanin | entry'],
20
+ ['--seed-count <n>', 'Number of seed nodes (default: 30)'],
21
+ ['--size-by <metric>', 'Size nodes by: uniform | fan-in | fan-out | complexity'],
22
+ ['--color-by <mode>', 'Color nodes by: kind | role | community | complexity'],
23
+ ],
24
+ async execute(_args, opts, ctx) {
25
+ const { generatePlotHTML, loadPlotConfig } = await import('../../features/graph-enrichment.js');
26
+ const os = await import('node:os');
27
+ const { db, close } = openGraph(opts);
28
+
29
+ let plotCfg;
30
+ let html;
31
+ try {
32
+ if (opts.config) {
33
+ try {
34
+ plotCfg = JSON.parse(fs.readFileSync(opts.config, 'utf-8'));
35
+ } catch (e) {
36
+ console.error(`Failed to load config: ${e.message}`);
37
+ process.exitCode = 1;
38
+ return;
39
+ }
40
+ } else {
41
+ plotCfg = loadPlotConfig(process.cwd());
42
+ }
43
+
44
+ if (opts.cluster) plotCfg.clusterBy = opts.cluster;
45
+ if (opts.colorBy) plotCfg.colorBy = opts.colorBy;
46
+ if (opts.sizeBy) plotCfg.sizeBy = opts.sizeBy;
47
+ if (opts.seed) plotCfg.seedStrategy = opts.seed;
48
+ if (opts.seedCount) plotCfg.seedCount = parseInt(opts.seedCount, 10);
49
+ if (opts.overlay) {
50
+ const parts = opts.overlay.split(',').map((s) => s.trim());
51
+ if (!plotCfg.overlays) plotCfg.overlays = {};
52
+ if (parts.includes('complexity')) plotCfg.overlays.complexity = true;
53
+ if (parts.includes('risk')) plotCfg.overlays.risk = true;
54
+ }
55
+
56
+ html = generatePlotHTML(db, {
57
+ fileLevel: !opts.functions,
58
+ noTests: ctx.resolveNoTests(opts),
59
+ minConfidence: parseFloat(opts.minConfidence),
60
+ config: plotCfg,
61
+ });
62
+ } finally {
63
+ close();
64
+ }
65
+
66
+ if (!html) {
67
+ console.error('generatePlotHTML returned no output');
68
+ process.exitCode = 1;
69
+ return;
70
+ }
71
+
72
+ const outPath = opts.output || path.join(os.tmpdir(), `codegraph-plot-${Date.now()}.html`);
73
+ fs.writeFileSync(outPath, html, 'utf-8');
74
+ console.log(`Plot written to ${outPath}`);
75
+
76
+ if (opts.open !== false) {
77
+ const { execFile } = await import('node:child_process');
78
+ const args =
79
+ process.platform === 'win32'
80
+ ? ['cmd', ['/c', 'start', '', outPath]]
81
+ : process.platform === 'darwin'
82
+ ? ['open', [outPath]]
83
+ : ['xdg-open', [outPath]];
84
+ execFile(args[0], args[1], (err) => {
85
+ if (err) console.error('Could not open browser:', err.message);
86
+ });
87
+ }
88
+ },
89
+ };
@@ -0,0 +1,45 @@
1
+ import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
2
+ import { fnDeps, symbolPath } from '../../presentation/queries-cli.js';
3
+
4
+ export const command = {
5
+ name: 'query <name>',
6
+ description: 'Function-level dependency chain or shortest path between symbols',
7
+ queryOpts: true,
8
+ options: [
9
+ ['--depth <n>', 'Transitive caller depth', '3'],
10
+ ['-f, --file <path>', 'Scope search to functions in this file (partial match)'],
11
+ ['-k, --kind <kind>', 'Filter to a specific symbol kind'],
12
+ ['--path <to>', 'Path mode: find shortest path to <to>'],
13
+ ['--kinds <kinds>', 'Path mode: comma-separated edge kinds to follow (default: calls)'],
14
+ ['--reverse', 'Path mode: follow edges backward'],
15
+ ['--from-file <path>', 'Path mode: disambiguate source symbol by file'],
16
+ ['--to-file <path>', 'Path mode: disambiguate target symbol by file'],
17
+ ],
18
+ validate([_name], opts) {
19
+ if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
20
+ return `Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`;
21
+ }
22
+ },
23
+ execute([name], opts, ctx) {
24
+ if (opts.path) {
25
+ console.error('Note: "query --path" is deprecated, use "codegraph path <from> <to>" instead');
26
+ symbolPath(name, opts.path, opts.db, {
27
+ maxDepth: opts.depth ? parseInt(opts.depth, 10) : 10,
28
+ edgeKinds: opts.kinds ? opts.kinds.split(',').map((s) => s.trim()) : undefined,
29
+ reverse: opts.reverse,
30
+ fromFile: opts.fromFile,
31
+ toFile: opts.toFile,
32
+ kind: opts.kind,
33
+ noTests: ctx.resolveNoTests(opts),
34
+ json: opts.json,
35
+ });
36
+ } else {
37
+ fnDeps(name, opts.db, {
38
+ depth: parseInt(opts.depth, 10),
39
+ file: opts.file,
40
+ kind: opts.kind,
41
+ ...ctx.resolveQueryOpts(opts),
42
+ });
43
+ }
44
+ },
45
+ };
@@ -0,0 +1,100 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ listRepos,
5
+ pruneRegistry,
6
+ REGISTRY_PATH,
7
+ registerRepo,
8
+ unregisterRepo,
9
+ } from '../../infrastructure/registry.js';
10
+ import { ConfigError } from '../../shared/errors.js';
11
+
12
+ export const command = {
13
+ name: 'registry',
14
+ description: 'Manage the multi-repo project registry',
15
+ subcommands: [
16
+ {
17
+ name: 'list',
18
+ description: 'List all registered repositories',
19
+ options: [['-j, --json', 'Output as JSON']],
20
+ execute(_args, opts) {
21
+ pruneRegistry();
22
+ const repos = listRepos();
23
+ if (opts.json) {
24
+ console.log(JSON.stringify(repos, null, 2));
25
+ } else if (repos.length === 0) {
26
+ console.log(`No repositories registered.\nRegistry: ${REGISTRY_PATH}`);
27
+ } else {
28
+ console.log(`Registered repositories (${REGISTRY_PATH}):\n`);
29
+ for (const r of repos) {
30
+ const dbExists = fs.existsSync(r.dbPath);
31
+ const status = dbExists ? '' : ' [DB missing]';
32
+ console.log(` ${r.name}${status}`);
33
+ console.log(` Path: ${r.path}`);
34
+ console.log(` DB: ${r.dbPath}`);
35
+ console.log();
36
+ }
37
+ }
38
+ },
39
+ },
40
+ {
41
+ name: 'add <dir>',
42
+ description: 'Register a project directory',
43
+ options: [['-n, --name <name>', 'Custom name (defaults to directory basename)']],
44
+ execute([dir], opts) {
45
+ const absDir = path.resolve(dir);
46
+ const { name, entry } = registerRepo(absDir, opts.name);
47
+ console.log(`Registered "${name}" → ${entry.path}`);
48
+ },
49
+ },
50
+ {
51
+ name: 'remove <name>',
52
+ description: 'Unregister a repository by name',
53
+ execute([name]) {
54
+ const removed = unregisterRepo(name);
55
+ if (removed) {
56
+ console.log(`Removed "${name}" from registry.`);
57
+ } else {
58
+ throw new ConfigError(`Repository "${name}" not found in registry.`);
59
+ }
60
+ },
61
+ },
62
+ {
63
+ name: 'prune',
64
+ description: 'Remove stale registry entries (missing directories or idle beyond TTL)',
65
+ options: [
66
+ ['--ttl <days>', 'Days of inactivity before pruning (default: 30)', '30'],
67
+ ['--exclude <names>', 'Comma-separated repo names to preserve from pruning'],
68
+ ['--dry-run', 'Show what would be pruned without removing anything'],
69
+ ],
70
+ execute(_args, opts) {
71
+ const excludeNames = opts.exclude
72
+ ? opts.exclude
73
+ .split(',')
74
+ .map((s) => s.trim())
75
+ .filter((s) => s.length > 0)
76
+ : [];
77
+ const dryRun = !!opts.dryRun;
78
+ const pruned = pruneRegistry(undefined, parseInt(opts.ttl, 10), excludeNames, dryRun);
79
+ if (pruned.length === 0) {
80
+ console.log('No stale entries found.');
81
+ } else {
82
+ const prefix = dryRun ? 'Would prune' : 'Pruned';
83
+ for (const entry of pruned) {
84
+ const tag = entry.reason === 'expired' ? 'expired' : 'missing';
85
+ console.log(`${prefix} "${entry.name}" (${entry.path}) [${tag}]`);
86
+ }
87
+ if (dryRun) {
88
+ console.log(
89
+ `\nDry run: ${pruned.length} ${pruned.length === 1 ? 'entry' : 'entries'} would be removed.`,
90
+ );
91
+ } else {
92
+ console.log(
93
+ `\nRemoved ${pruned.length} stale ${pruned.length === 1 ? 'entry' : 'entries'}.`,
94
+ );
95
+ }
96
+ }
97
+ },
98
+ },
99
+ ],
100
+ };
@@ -0,0 +1,30 @@
1
+ import { VALID_ROLES } from '../../domain/queries.js';
2
+ import { roles } from '../../presentation/queries-cli.js';
3
+
4
+ export const command = {
5
+ name: 'roles',
6
+ description: 'Show node role classification: entry, core, utility, adapter, dead, leaf',
7
+ options: [
8
+ ['-d, --db <path>', 'Path to graph.db'],
9
+ ['--role <role>', `Filter by role (${VALID_ROLES.join(', ')})`],
10
+ ['-f, --file <path>', 'Scope to a specific file (partial match)'],
11
+ ['-T, --no-tests', 'Exclude test/spec files'],
12
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
13
+ ['-j, --json', 'Output as JSON'],
14
+ ['--limit <number>', 'Max results to return'],
15
+ ['--offset <number>', 'Skip N results (default: 0)'],
16
+ ['--ndjson', 'Newline-delimited JSON output'],
17
+ ],
18
+ validate(_args, opts) {
19
+ if (opts.role && !VALID_ROLES.includes(opts.role)) {
20
+ return `Invalid role "${opts.role}". Valid roles: ${VALID_ROLES.join(', ')}`;
21
+ }
22
+ },
23
+ execute(_args, opts, ctx) {
24
+ roles(opts.db, {
25
+ role: opts.role,
26
+ file: opts.file,
27
+ ...ctx.resolveQueryOpts(opts),
28
+ });
29
+ },
30
+ };
@@ -0,0 +1,42 @@
1
+ import { search } from '../../domain/search/index.js';
2
+
3
+ export const command = {
4
+ name: 'search <query>',
5
+ description: 'Semantic search: find functions by natural language description',
6
+ options: [
7
+ ['-d, --db <path>', 'Path to graph.db'],
8
+ ['-m, --model <name>', 'Override embedding model (auto-detects from DB)'],
9
+ ['-n, --limit <number>', 'Max results', '15'],
10
+ ['-T, --no-tests', 'Exclude test/spec files from results'],
11
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
12
+ ['--min-score <score>', 'Minimum similarity threshold', '0.2'],
13
+ ['-k, --kind <kind>', 'Filter by kind: function, method, class'],
14
+ ['--file <pattern>', 'Filter by file path pattern'],
15
+ ['--rrf-k <number>', 'RRF k parameter for multi-query ranking', '60'],
16
+ ['--mode <mode>', 'Search mode: hybrid, semantic, keyword (default: hybrid)'],
17
+ ['-j, --json', 'Output as JSON'],
18
+ ['--offset <number>', 'Skip N results (default: 0)'],
19
+ ['--ndjson', 'Newline-delimited JSON output'],
20
+ ],
21
+ validate([_query], opts) {
22
+ const validModes = ['hybrid', 'semantic', 'keyword'];
23
+ if (opts.mode && !validModes.includes(opts.mode)) {
24
+ return `Invalid mode "${opts.mode}". Valid: ${validModes.join(', ')}`;
25
+ }
26
+ },
27
+ async execute([query], opts, ctx) {
28
+ await search(query, opts.db, {
29
+ limit: parseInt(opts.limit, 10),
30
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
31
+ noTests: ctx.resolveNoTests(opts),
32
+ minScore: parseFloat(opts.minScore),
33
+ model: opts.model,
34
+ kind: opts.kind,
35
+ filePattern: opts.file,
36
+ rrfK: parseInt(opts.rrfK, 10),
37
+ mode: opts.mode,
38
+ json: opts.json,
39
+ ndjson: opts.ndjson,
40
+ });
41
+ },
42
+ };
@@ -0,0 +1,28 @@
1
+ import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
2
+
3
+ export const command = {
4
+ name: 'sequence <name>',
5
+ description: 'Generate a Mermaid sequence diagram from call graph edges (participants = files)',
6
+ queryOpts: true,
7
+ options: [
8
+ ['--depth <n>', 'Max forward traversal depth', '10'],
9
+ ['--dataflow', 'Annotate with parameter names and return arrows from dataflow table'],
10
+ ['-f, --file <path>', 'Scope to a specific file (partial match)'],
11
+ ['-k, --kind <kind>', 'Filter by symbol kind'],
12
+ ],
13
+ validate([_name], opts) {
14
+ if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
15
+ return `Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`;
16
+ }
17
+ },
18
+ async execute([name], opts, ctx) {
19
+ const { sequence } = await import('../../presentation/sequence.js');
20
+ sequence(name, opts.db, {
21
+ depth: parseInt(opts.depth, 10),
22
+ file: opts.file,
23
+ kind: opts.kind,
24
+ dataflow: opts.dataflow,
25
+ ...ctx.resolveQueryOpts(opts),
26
+ });
27
+ },
28
+ };
@@ -0,0 +1,66 @@
1
+ import {
2
+ snapshotDelete,
3
+ snapshotList,
4
+ snapshotRestore,
5
+ snapshotSave,
6
+ } from '../../features/snapshot.js';
7
+
8
+ export const command = {
9
+ name: 'snapshot',
10
+ description: 'Save and restore graph database snapshots',
11
+ subcommands: [
12
+ {
13
+ name: 'save <name>',
14
+ description: 'Save a snapshot of the current graph database',
15
+ options: [
16
+ ['-d, --db <path>', 'Path to graph.db'],
17
+ ['--force', 'Overwrite existing snapshot'],
18
+ ],
19
+ execute([name], opts, ctx) {
20
+ const result = snapshotSave(name, { dbPath: opts.db, force: opts.force });
21
+ console.log(`Snapshot saved: ${result.name} (${ctx.formatSize(result.size)})`);
22
+ },
23
+ },
24
+ {
25
+ name: 'restore <name>',
26
+ description: 'Restore a snapshot over the current graph database',
27
+ options: [['-d, --db <path>', 'Path to graph.db']],
28
+ execute([name], opts) {
29
+ snapshotRestore(name, { dbPath: opts.db });
30
+ console.log(`Snapshot "${name}" restored.`);
31
+ },
32
+ },
33
+ {
34
+ name: 'list',
35
+ description: 'List all saved snapshots',
36
+ options: [
37
+ ['-d, --db <path>', 'Path to graph.db'],
38
+ ['-j, --json', 'Output as JSON'],
39
+ ],
40
+ execute(_args, opts, ctx) {
41
+ const snapshots = snapshotList({ dbPath: opts.db });
42
+ if (opts.json) {
43
+ console.log(JSON.stringify(snapshots, null, 2));
44
+ } else if (snapshots.length === 0) {
45
+ console.log('No snapshots found.');
46
+ } else {
47
+ console.log(`Snapshots (${snapshots.length}):\n`);
48
+ for (const s of snapshots) {
49
+ console.log(
50
+ ` ${s.name.padEnd(30)} ${ctx.formatSize(s.size).padStart(10)} ${s.createdAt.toISOString()}`,
51
+ );
52
+ }
53
+ }
54
+ },
55
+ },
56
+ {
57
+ name: 'delete <name>',
58
+ description: 'Delete a saved snapshot',
59
+ options: [['-d, --db <path>', 'Path to graph.db']],
60
+ execute([name], opts) {
61
+ snapshotDelete(name, { dbPath: opts.db });
62
+ console.log(`Snapshot "${name}" deleted.`);
63
+ },
64
+ },
65
+ ],
66
+ };
@@ -0,0 +1,15 @@
1
+ import { stats } from '../../presentation/queries-cli.js';
2
+
3
+ export const command = {
4
+ name: 'stats',
5
+ description: 'Show graph health overview: nodes, edges, languages, cycles, hotspots, embeddings',
6
+ options: [
7
+ ['-d, --db <path>', 'Path to graph.db'],
8
+ ['-T, --no-tests', 'Exclude test/spec files from results'],
9
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
10
+ ['-j, --json', 'Output as JSON'],
11
+ ],
12
+ async execute(_args, opts, ctx) {
13
+ await stats(opts.db, { noTests: ctx.resolveNoTests(opts), json: opts.json });
14
+ },
15
+ };
@@ -0,0 +1,33 @@
1
+ export const command = {
2
+ name: 'structure [dir]',
3
+ description:
4
+ 'Show project directory structure with hierarchy, cohesion scores, and per-file metrics',
5
+ options: [
6
+ ['-d, --db <path>', 'Path to graph.db'],
7
+ ['--depth <n>', 'Max directory depth'],
8
+ ['--sort <metric>', 'Sort by: cohesion | fan-in | fan-out | density | files', 'files'],
9
+ ['--full', 'Show all files without limit'],
10
+ ['-T, --no-tests', 'Exclude test/spec files'],
11
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
12
+ ['-j, --json', 'Output as JSON'],
13
+ ['--limit <number>', 'Max results to return'],
14
+ ['--offset <number>', 'Skip N results (default: 0)'],
15
+ ['--ndjson', 'Newline-delimited JSON output'],
16
+ ],
17
+ async execute([dir], opts, ctx) {
18
+ const { structureData, formatStructure } = await import('../../presentation/structure.js');
19
+ const qOpts = ctx.resolveQueryOpts(opts);
20
+ const data = structureData(opts.db, {
21
+ directory: dir,
22
+ depth: opts.depth ? parseInt(opts.depth, 10) : undefined,
23
+ sort: opts.sort,
24
+ full: opts.full,
25
+ noTests: qOpts.noTests,
26
+ limit: qOpts.limit,
27
+ offset: qOpts.offset,
28
+ });
29
+ if (!ctx.outputResult(data, 'directories', opts)) {
30
+ console.log(formatStructure(data));
31
+ }
32
+ },
33
+ };
@@ -0,0 +1,78 @@
1
+ import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../domain/queries.js';
2
+ import { ConfigError } from '../../shared/errors.js';
3
+
4
+ export const command = {
5
+ name: 'triage',
6
+ description:
7
+ 'Ranked audit queue by composite risk score (connectivity + complexity + churn + role)',
8
+ options: [
9
+ ['-d, --db <path>', 'Path to graph.db'],
10
+ ['-n, --limit <number>', 'Max results to return', '20'],
11
+ [
12
+ '--level <level>',
13
+ 'Granularity: function (default) | file | directory. File/directory level shows hotspots',
14
+ 'function',
15
+ ],
16
+ [
17
+ '--sort <metric>',
18
+ 'Sort metric: risk | complexity | churn | fan-in | mi (function level); fan-in | fan-out | density | coupling (file/directory level)',
19
+ 'risk',
20
+ ],
21
+ ['--min-score <score>', 'Only show symbols with risk score >= threshold'],
22
+ ['--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)'],
23
+ ['-f, --file <path>', 'Scope to a specific file (partial match)'],
24
+ ['-k, --kind <kind>', 'Filter by symbol kind (function, method, class)'],
25
+ ['-T, --no-tests', 'Exclude test/spec files from results'],
26
+ ['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
27
+ ['-j, --json', 'Output as JSON'],
28
+ ['--offset <number>', 'Skip N results (default: 0)'],
29
+ ['--ndjson', 'Newline-delimited JSON output'],
30
+ ['--weights <json>', 'Custom weights JSON (e.g. \'{"fanIn":1,"complexity":0}\')'],
31
+ ],
32
+ async execute(_args, opts, ctx) {
33
+ if (opts.level === 'file' || opts.level === 'directory') {
34
+ const { hotspotsData, formatHotspots } = await import('../../presentation/structure.js');
35
+ const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
36
+ const data = hotspotsData(opts.db, {
37
+ metric,
38
+ level: opts.level,
39
+ limit: parseInt(opts.limit, 10),
40
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
41
+ noTests: ctx.resolveNoTests(opts),
42
+ });
43
+ if (!ctx.outputResult(data, 'hotspots', opts)) {
44
+ console.log(formatHotspots(data));
45
+ }
46
+ return;
47
+ }
48
+
49
+ if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
50
+ throw new ConfigError(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
51
+ }
52
+ if (opts.role && !VALID_ROLES.includes(opts.role)) {
53
+ throw new ConfigError(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
54
+ }
55
+ let weights;
56
+ if (opts.weights) {
57
+ try {
58
+ weights = JSON.parse(opts.weights);
59
+ } catch (err) {
60
+ throw new ConfigError('Invalid --weights JSON', { cause: err });
61
+ }
62
+ }
63
+ const { triage } = await import('../../presentation/triage.js');
64
+ triage(opts.db, {
65
+ limit: parseInt(opts.limit, 10),
66
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
67
+ sort: opts.sort,
68
+ minScore: opts.minScore,
69
+ role: opts.role,
70
+ file: opts.file,
71
+ kind: opts.kind,
72
+ noTests: ctx.resolveNoTests(opts),
73
+ json: opts.json,
74
+ ndjson: opts.ndjson,
75
+ weights,
76
+ });
77
+ },
78
+ };
@@ -0,0 +1,12 @@
1
+ import path from 'node:path';
2
+ import { watchProject } from '../../domain/graph/watcher.js';
3
+
4
+ export const command = {
5
+ name: 'watch [dir]',
6
+ description: 'Watch project for file changes and incrementally update the graph',
7
+ async execute([dir], _opts, ctx) {
8
+ const root = path.resolve(dir || '.');
9
+ const engine = ctx.program.opts().engine;
10
+ await watchProject(root, { engine });
11
+ },
12
+ };
@@ -0,0 +1,20 @@
1
+ import { where } from '../../presentation/queries-cli.js';
2
+
3
+ export const command = {
4
+ name: 'where [name]',
5
+ description: 'Find where a symbol is defined and used (minimal, fast lookup)',
6
+ queryOpts: true,
7
+ options: [['-f, --file <path>', 'File overview: list symbols, imports, exports']],
8
+ validate([name], opts) {
9
+ if (!name && !opts.file) {
10
+ return 'Provide a symbol name or use --file <path>';
11
+ }
12
+ },
13
+ execute([name], opts, ctx) {
14
+ const target = opts.file || name;
15
+ where(target, opts.db, {
16
+ file: !!opts.file,
17
+ ...ctx.resolveQueryOpts(opts),
18
+ });
19
+ },
20
+ };