@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
@@ -1,7 +1,60 @@
1
+ import { execFileSync } from 'node:child_process';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import Database from 'better-sqlite3';
4
- import { warn } from '../logger.js';
5
+ import { debug, warn } from '../infrastructure/logger.js';
6
+ import { DbError } from '../shared/errors.js';
7
+ import { Repository } from './repository/base.js';
8
+ import { SqliteRepository } from './repository/sqlite-repository.js';
9
+
10
+ let _cachedRepoRoot; // undefined = not computed, null = not a git repo
11
+ let _cachedRepoRootCwd; // cwd at the time the cache was populated
12
+
13
+ /**
14
+ * Return the git worktree/repo root for the given directory (or cwd).
15
+ * Uses `git rev-parse --show-toplevel` which returns the correct root
16
+ * for both regular repos and git worktrees.
17
+ * Results are cached per-process when called without arguments.
18
+ * The cache is keyed on cwd so it invalidates if the working directory changes
19
+ * (e.g. MCP server serving multiple sessions).
20
+ * @param {string} [fromDir] - Directory to resolve from (defaults to cwd)
21
+ * @returns {string | null} Absolute path to repo root, or null if not in a git repo
22
+ */
23
+ export function findRepoRoot(fromDir) {
24
+ const dir = fromDir || process.cwd();
25
+ if (!fromDir && _cachedRepoRoot !== undefined && _cachedRepoRootCwd === dir) {
26
+ return _cachedRepoRoot;
27
+ }
28
+ let root = null;
29
+ try {
30
+ const raw = execFileSync('git', ['rev-parse', '--show-toplevel'], {
31
+ cwd: dir,
32
+ encoding: 'utf-8',
33
+ stdio: ['pipe', 'pipe', 'pipe'],
34
+ }).trim();
35
+ // Use realpathSync to resolve symlinks (macOS /var → /private/var) and
36
+ // 8.3 short names (Windows RUNNER~1 → runneradmin) so the ceiling path
37
+ // matches the realpathSync'd dir in findDbPath.
38
+ try {
39
+ root = fs.realpathSync(raw);
40
+ } catch {
41
+ root = path.resolve(raw);
42
+ }
43
+ } catch {
44
+ root = null;
45
+ }
46
+ if (!fromDir) {
47
+ _cachedRepoRoot = root;
48
+ _cachedRepoRootCwd = dir;
49
+ }
50
+ return root;
51
+ }
52
+
53
+ /** Reset the cached repo root (for testing). */
54
+ export function _resetRepoRootCache() {
55
+ _cachedRepoRoot = undefined;
56
+ _cachedRepoRootCwd = undefined;
57
+ }
5
58
 
6
59
  function isProcessAlive(pid) {
7
60
  try {
@@ -43,6 +96,22 @@ function releaseAdvisoryLock(lockPath) {
43
96
  }
44
97
  }
45
98
 
99
+ /**
100
+ * Check if two paths refer to the same directory.
101
+ * Handles Windows 8.3 short names (RUNNER~1 vs runneradmin) and macOS
102
+ * symlinks (/tmp vs /private/tmp) where string comparison fails.
103
+ */
104
+ function isSameDirectory(a, b) {
105
+ if (path.resolve(a) === path.resolve(b)) return true;
106
+ try {
107
+ const sa = fs.statSync(a);
108
+ const sb = fs.statSync(b);
109
+ return sa.dev === sb.dev && sa.ino === sb.ino;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
46
115
  export function openDb(dbPath) {
47
116
  const dir = path.dirname(dbPath);
48
117
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -61,15 +130,41 @@ export function closeDb(db) {
61
130
 
62
131
  export function findDbPath(customPath) {
63
132
  if (customPath) return path.resolve(customPath);
64
- let dir = process.cwd();
133
+ const rawCeiling = findRepoRoot();
134
+ // Normalize ceiling with realpathSync to resolve 8.3 short names (Windows
135
+ // RUNNER~1 → runneradmin) and symlinks (macOS /var → /private/var).
136
+ // findRepoRoot already applies realpathSync internally, but the git output
137
+ // may still contain short names on some Windows CI environments.
138
+ let ceiling;
139
+ if (rawCeiling) {
140
+ try {
141
+ ceiling = fs.realpathSync(rawCeiling);
142
+ } catch {
143
+ ceiling = rawCeiling;
144
+ }
145
+ } else {
146
+ ceiling = null;
147
+ }
148
+ // Resolve symlinks (e.g. macOS /var → /private/var) so dir matches ceiling from git
149
+ let dir;
150
+ try {
151
+ dir = fs.realpathSync(process.cwd());
152
+ } catch {
153
+ dir = process.cwd();
154
+ }
65
155
  while (true) {
66
156
  const candidate = path.join(dir, '.codegraph', 'graph.db');
67
157
  if (fs.existsSync(candidate)) return candidate;
158
+ if (ceiling && isSameDirectory(dir, ceiling)) {
159
+ debug(`findDbPath: stopped at git ceiling ${ceiling}`);
160
+ break;
161
+ }
68
162
  const parent = path.dirname(dir);
69
163
  if (parent === dir) break;
70
164
  dir = parent;
71
165
  }
72
- return path.join(process.cwd(), '.codegraph', 'graph.db');
166
+ const base = ceiling || process.cwd();
167
+ return path.join(base, '.codegraph', 'graph.db');
73
168
  }
74
169
 
75
170
  /**
@@ -78,11 +173,39 @@ export function findDbPath(customPath) {
78
173
  export function openReadonlyOrFail(customPath) {
79
174
  const dbPath = findDbPath(customPath);
80
175
  if (!fs.existsSync(dbPath)) {
81
- console.error(
82
- `No codegraph database found at ${dbPath}.\n` +
83
- `Run "codegraph build" first to analyze your codebase.`,
176
+ throw new DbError(
177
+ `No codegraph database found at ${dbPath}.\nRun "codegraph build" first to analyze your codebase.`,
178
+ { file: dbPath },
84
179
  );
85
- process.exit(1);
86
180
  }
87
181
  return new Database(dbPath, { readonly: true });
88
182
  }
183
+
184
+ /**
185
+ * Open a Repository from either an injected instance or a DB path.
186
+ *
187
+ * When `opts.repo` is a Repository instance, returns it directly (no DB opened).
188
+ * Otherwise opens a readonly SQLite DB and wraps it in SqliteRepository.
189
+ *
190
+ * @param {string} [customDbPath] - Path to graph.db (ignored when opts.repo is set)
191
+ * @param {object} [opts]
192
+ * @param {Repository} [opts.repo] - Pre-built Repository to use instead of SQLite
193
+ * @returns {{ repo: Repository, close(): void }}
194
+ */
195
+ export function openRepo(customDbPath, opts = {}) {
196
+ if (opts.repo != null) {
197
+ if (!(opts.repo instanceof Repository)) {
198
+ throw new TypeError(
199
+ `openRepo: opts.repo must be a Repository instance, got ${Object.prototype.toString.call(opts.repo)}`,
200
+ );
201
+ }
202
+ return { repo: opts.repo, close() {} };
203
+ }
204
+ const db = openReadonlyOrFail(customDbPath);
205
+ return {
206
+ repo: new SqliteRepository(db),
207
+ close() {
208
+ db.close();
209
+ },
210
+ };
211
+ }
@@ -1,13 +1,20 @@
1
- // Barrel re-export — keeps all existing `import { ... } from './db.js'` working.
2
- export { closeDb, findDbPath, openDb, openReadonlyOrFail } from './db/connection.js';
3
- export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './db/migrations.js';
1
+ // Barrel re-export — keeps all existing `import { ... } from '…/db/index.js'` working.
2
+ export {
3
+ closeDb,
4
+ findDbPath,
5
+ findRepoRoot,
6
+ openDb,
7
+ openReadonlyOrFail,
8
+ openRepo,
9
+ } from './connection.js';
10
+ export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
4
11
  export {
5
12
  fanInJoinSQL,
6
13
  fanOutJoinSQL,
7
14
  kindInClause,
8
15
  NodeQuery,
9
16
  testFilterSQL,
10
- } from './db/query-builder.js';
17
+ } from './query-builder.js';
11
18
  export {
12
19
  bulkNodeIdsByFile,
13
20
  countCrossFileCallers,
@@ -29,8 +36,10 @@ export {
29
36
  findImportTargets,
30
37
  findIntraFileCallEdges,
31
38
  findNodeById,
39
+ findNodeByQualifiedName,
32
40
  findNodeChildren,
33
41
  findNodesByFile,
42
+ findNodesByScope,
34
43
  findNodesForTriage,
35
44
  findNodesWithFanIn,
36
45
  getCallableNodes,
@@ -50,9 +59,12 @@ export {
50
59
  hasCoChanges,
51
60
  hasDataflowTable,
52
61
  hasEmbeddings,
62
+ InMemoryRepository,
53
63
  iterateFunctionNodes,
54
64
  listFunctionNodes,
55
65
  purgeFileData,
56
66
  purgeFilesData,
67
+ Repository,
68
+ SqliteRepository,
57
69
  upsertCoChangeMeta,
58
- } from './db/repository/index.js';
70
+ } from './repository/index.js';
@@ -1,4 +1,4 @@
1
- import { debug } from '../logger.js';
1
+ import { debug } from '../infrastructure/logger.js';
2
2
 
3
3
  // ─── Schema Migrations ─────────────────────────────────────────────────
4
4
  export const MIGRATIONS = [
@@ -229,6 +229,17 @@ export const MIGRATIONS = [
229
229
  CREATE INDEX IF NOT EXISTS idx_nodes_exported ON nodes(exported);
230
230
  `,
231
231
  },
232
+ {
233
+ version: 15,
234
+ up: `
235
+ ALTER TABLE nodes ADD COLUMN qualified_name TEXT;
236
+ ALTER TABLE nodes ADD COLUMN scope TEXT;
237
+ ALTER TABLE nodes ADD COLUMN visibility TEXT;
238
+ UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL;
239
+ CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name);
240
+ CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope);
241
+ `,
242
+ },
232
243
  ];
233
244
 
234
245
  export function getBuildMeta(db, key) {
@@ -309,4 +320,34 @@ export function initSchema(db) {
309
320
  } catch {
310
321
  /* already exists */
311
322
  }
323
+ try {
324
+ db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
325
+ } catch {
326
+ /* already exists */
327
+ }
328
+ try {
329
+ db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
330
+ } catch {
331
+ /* already exists */
332
+ }
333
+ try {
334
+ db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
335
+ } catch {
336
+ /* already exists */
337
+ }
338
+ try {
339
+ db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
340
+ } catch {
341
+ /* nodes table may not exist yet */
342
+ }
343
+ try {
344
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
345
+ } catch {
346
+ /* already exists */
347
+ }
348
+ try {
349
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
350
+ } catch {
351
+ /* already exists */
352
+ }
312
353
  }
@@ -1,4 +1,5 @@
1
- import { EVERY_EDGE_KIND } from '../kinds.js';
1
+ import { DbError } from '../shared/errors.js';
2
+ import { EVERY_EDGE_KIND } from '../shared/kinds.js';
2
3
 
3
4
  // ─── Validation Helpers ─────────────────────────────────────────────
4
5
 
@@ -12,13 +13,13 @@ const SAFE_SELECT_TOKEN_RE =
12
13
 
13
14
  function validateAlias(alias) {
14
15
  if (!SAFE_ALIAS_RE.test(alias)) {
15
- throw new Error(`Invalid SQL alias: ${alias}`);
16
+ throw new DbError(`Invalid SQL alias: ${alias}`);
16
17
  }
17
18
  }
18
19
 
19
20
  function validateColumn(column) {
20
21
  if (!SAFE_COLUMN_RE.test(column)) {
21
- throw new Error(`Invalid SQL column: ${column}`);
22
+ throw new DbError(`Invalid SQL column: ${column}`);
22
23
  }
23
24
  }
24
25
 
@@ -26,7 +27,7 @@ function validateOrderBy(clause) {
26
27
  const terms = clause.split(',').map((t) => t.trim());
27
28
  for (const term of terms) {
28
29
  if (!SAFE_ORDER_TERM_RE.test(term)) {
29
- throw new Error(`Invalid ORDER BY term: ${term}`);
30
+ throw new DbError(`Invalid ORDER BY term: ${term}`);
30
31
  }
31
32
  }
32
33
  }
@@ -51,19 +52,26 @@ function validateSelectCols(cols) {
51
52
  const tokens = splitTopLevelCommas(cols);
52
53
  for (const token of tokens) {
53
54
  if (!SAFE_SELECT_TOKEN_RE.test(token)) {
54
- throw new Error(`Invalid SELECT expression: ${token}`);
55
+ throw new DbError(`Invalid SELECT expression: ${token}`);
55
56
  }
56
57
  }
57
58
  }
58
59
 
59
60
  function validateEdgeKind(edgeKind) {
60
61
  if (!EVERY_EDGE_KIND.includes(edgeKind)) {
61
- throw new Error(
62
+ throw new DbError(
62
63
  `Invalid edge kind: ${edgeKind} (expected one of ${EVERY_EDGE_KIND.join(', ')})`,
63
64
  );
64
65
  }
65
66
  }
66
67
 
68
+ // ─── LIKE Escaping ──────────────────────────────────────────────────
69
+
70
+ /** Escape LIKE wildcards in a literal string segment. */
71
+ export function escapeLike(s) {
72
+ return s.replace(/[%_\\]/g, '\\$&');
73
+ }
74
+
67
75
  // ─── Standalone Helpers ──────────────────────────────────────────────
68
76
 
69
77
  /**
@@ -163,11 +171,11 @@ export class NodeQuery {
163
171
  return this;
164
172
  }
165
173
 
166
- /** WHERE n.file LIKE ? (no-op if falsy). */
174
+ /** WHERE n.file LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
167
175
  fileFilter(file) {
168
176
  if (!file) return this;
169
- this.#conditions.push('n.file LIKE ?');
170
- this.#params.push(`%${file}%`);
177
+ this.#conditions.push("n.file LIKE ? ESCAPE '\\'");
178
+ this.#params.push(`%${escapeLike(file)}%`);
171
179
  return this;
172
180
  }
173
181
 
@@ -187,11 +195,11 @@ export class NodeQuery {
187
195
  return this;
188
196
  }
189
197
 
190
- /** WHERE n.name LIKE ? (no-op if falsy). */
198
+ /** WHERE n.name LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
191
199
  nameLike(pattern) {
192
200
  if (!pattern) return this;
193
- this.#conditions.push('n.name LIKE ?');
194
- this.#params.push(`%${pattern}%`);
201
+ this.#conditions.push("n.name LIKE ? ESCAPE '\\'");
202
+ this.#params.push(`%${escapeLike(pattern)}%`);
195
203
  return this;
196
204
  }
197
205
 
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Abstract Repository base class.
3
+ *
4
+ * Defines the contract for all graph data access. Every method throws
5
+ * "not implemented" by default — concrete subclasses override what they support.
6
+ */
7
+ export class Repository {
8
+ // ── Node lookups ────────────────────────────────────────────────────
9
+ /** @param {number} id @returns {object|undefined} */
10
+ findNodeById(_id) {
11
+ throw new Error('not implemented');
12
+ }
13
+
14
+ /** @param {string} file @returns {object[]} */
15
+ findNodesByFile(_file) {
16
+ throw new Error('not implemented');
17
+ }
18
+
19
+ /** @param {string} fileLike @returns {object[]} */
20
+ findFileNodes(_fileLike) {
21
+ throw new Error('not implemented');
22
+ }
23
+
24
+ /** @param {string} namePattern @param {object} [opts] @returns {object[]} */
25
+ findNodesWithFanIn(_namePattern, _opts) {
26
+ throw new Error('not implemented');
27
+ }
28
+
29
+ /** @returns {number} */
30
+ countNodes() {
31
+ throw new Error('not implemented');
32
+ }
33
+
34
+ /** @returns {number} */
35
+ countEdges() {
36
+ throw new Error('not implemented');
37
+ }
38
+
39
+ /** @returns {number} */
40
+ countFiles() {
41
+ throw new Error('not implemented');
42
+ }
43
+
44
+ /** @param {string} name @param {string} kind @param {string} file @param {number} line @returns {number|undefined} */
45
+ getNodeId(_name, _kind, _file, _line) {
46
+ throw new Error('not implemented');
47
+ }
48
+
49
+ /** @param {string} name @param {string} file @param {number} line @returns {number|undefined} */
50
+ getFunctionNodeId(_name, _file, _line) {
51
+ throw new Error('not implemented');
52
+ }
53
+
54
+ /** @param {string} file @returns {{ id: number, name: string, kind: string, line: number }[]} */
55
+ bulkNodeIdsByFile(_file) {
56
+ throw new Error('not implemented');
57
+ }
58
+
59
+ /** @param {number} parentId @returns {object[]} */
60
+ findNodeChildren(_parentId) {
61
+ throw new Error('not implemented');
62
+ }
63
+
64
+ /** @param {string} scopeName @param {object} [opts] @returns {object[]} */
65
+ findNodesByScope(_scopeName, _opts) {
66
+ throw new Error('not implemented');
67
+ }
68
+
69
+ /** @param {string} qualifiedName @param {object} [opts] @returns {object[]} */
70
+ findNodeByQualifiedName(_qualifiedName, _opts) {
71
+ throw new Error('not implemented');
72
+ }
73
+
74
+ /** @param {object} [opts] @returns {object[]} */
75
+ listFunctionNodes(_opts) {
76
+ throw new Error('not implemented');
77
+ }
78
+
79
+ /** @param {object} [opts] @returns {IterableIterator} */
80
+ iterateFunctionNodes(_opts) {
81
+ throw new Error('not implemented');
82
+ }
83
+
84
+ /** @param {object} [opts] @returns {object[]} */
85
+ findNodesForTriage(_opts) {
86
+ throw new Error('not implemented');
87
+ }
88
+
89
+ // ── Edge queries ────────────────────────────────────────────────────
90
+ /** @param {number} nodeId @returns {object[]} */
91
+ findCallees(_nodeId) {
92
+ throw new Error('not implemented');
93
+ }
94
+
95
+ /** @param {number} nodeId @returns {object[]} */
96
+ findCallers(_nodeId) {
97
+ throw new Error('not implemented');
98
+ }
99
+
100
+ /** @param {number} nodeId @returns {object[]} */
101
+ findDistinctCallers(_nodeId) {
102
+ throw new Error('not implemented');
103
+ }
104
+
105
+ /** @param {number} nodeId @returns {object[]} */
106
+ findAllOutgoingEdges(_nodeId) {
107
+ throw new Error('not implemented');
108
+ }
109
+
110
+ /** @param {number} nodeId @returns {object[]} */
111
+ findAllIncomingEdges(_nodeId) {
112
+ throw new Error('not implemented');
113
+ }
114
+
115
+ /** @param {number} nodeId @returns {string[]} */
116
+ findCalleeNames(_nodeId) {
117
+ throw new Error('not implemented');
118
+ }
119
+
120
+ /** @param {number} nodeId @returns {string[]} */
121
+ findCallerNames(_nodeId) {
122
+ throw new Error('not implemented');
123
+ }
124
+
125
+ /** @param {number} nodeId @returns {{ file: string, edge_kind: string }[]} */
126
+ findImportTargets(_nodeId) {
127
+ throw new Error('not implemented');
128
+ }
129
+
130
+ /** @param {number} nodeId @returns {{ file: string, edge_kind: string }[]} */
131
+ findImportSources(_nodeId) {
132
+ throw new Error('not implemented');
133
+ }
134
+
135
+ /** @param {number} nodeId @returns {object[]} */
136
+ findImportDependents(_nodeId) {
137
+ throw new Error('not implemented');
138
+ }
139
+
140
+ /** @param {string} file @returns {Set<number>} */
141
+ findCrossFileCallTargets(_file) {
142
+ throw new Error('not implemented');
143
+ }
144
+
145
+ /** @param {number} nodeId @param {string} file @returns {number} */
146
+ countCrossFileCallers(_nodeId, _file) {
147
+ throw new Error('not implemented');
148
+ }
149
+
150
+ /** @param {number} classNodeId @returns {Set<number>} */
151
+ getClassHierarchy(_classNodeId) {
152
+ throw new Error('not implemented');
153
+ }
154
+
155
+ /** @param {string} file @returns {{ caller_name: string, callee_name: string }[]} */
156
+ findIntraFileCallEdges(_file) {
157
+ throw new Error('not implemented');
158
+ }
159
+
160
+ // ── Graph-read queries ──────────────────────────────────────────────
161
+ /** @returns {{ id: number, name: string, kind: string, file: string }[]} */
162
+ getCallableNodes() {
163
+ throw new Error('not implemented');
164
+ }
165
+
166
+ /** @returns {{ source_id: number, target_id: number, confidence: number|null }[]} */
167
+ getCallEdges() {
168
+ throw new Error('not implemented');
169
+ }
170
+
171
+ /** @returns {{ id: number, name: string, file: string }[]} */
172
+ getFileNodesAll() {
173
+ throw new Error('not implemented');
174
+ }
175
+
176
+ /** @returns {{ source_id: number, target_id: number }[]} */
177
+ getImportEdges() {
178
+ throw new Error('not implemented');
179
+ }
180
+
181
+ // ── Optional table checks (default: false/undefined) ────────────────
182
+ /** @returns {boolean} */
183
+ hasCfgTables() {
184
+ throw new Error('not implemented');
185
+ }
186
+
187
+ /** @returns {boolean} */
188
+ hasEmbeddings() {
189
+ throw new Error('not implemented');
190
+ }
191
+
192
+ /** @returns {boolean} */
193
+ hasDataflowTable() {
194
+ throw new Error('not implemented');
195
+ }
196
+
197
+ /** @param {number} nodeId @returns {object|undefined} */
198
+ getComplexityForNode(_nodeId) {
199
+ throw new Error('not implemented');
200
+ }
201
+ }
@@ -1,3 +1,4 @@
1
+ import { CORE_SYMBOL_KINDS } from '../../shared/kinds.js';
1
2
  import { cachedStmt } from './cached-stmt.js';
2
3
 
3
4
  // ─── Statement caches (one prepared statement per db instance) ────────────
@@ -6,8 +7,10 @@ const _getCallEdgesStmt = new WeakMap();
6
7
  const _getFileNodesAllStmt = new WeakMap();
7
8
  const _getImportEdgesStmt = new WeakMap();
8
9
 
10
+ const CALLABLE_KINDS_SQL = CORE_SYMBOL_KINDS.map((k) => `'${k}'`).join(',');
11
+
9
12
  /**
10
- * Get callable nodes (function/method/class) for community detection.
13
+ * Get callable nodes (all core symbol kinds) for graph construction.
11
14
  * @param {object} db
12
15
  * @returns {{ id: number, name: string, kind: string, file: string }[]}
13
16
  */
@@ -15,20 +18,20 @@ export function getCallableNodes(db) {
15
18
  return cachedStmt(
16
19
  _getCallableNodesStmt,
17
20
  db,
18
- "SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')",
21
+ `SELECT id, name, kind, file FROM nodes WHERE kind IN (${CALLABLE_KINDS_SQL})`,
19
22
  ).all();
20
23
  }
21
24
 
22
25
  /**
23
26
  * Get all 'calls' edges.
24
27
  * @param {object} db
25
- * @returns {{ source_id: number, target_id: number }[]}
28
+ * @returns {{ source_id: number, target_id: number, confidence: number|null }[]}
26
29
  */
27
30
  export function getCallEdges(db) {
28
31
  return cachedStmt(
29
32
  _getCallEdgesStmt,
30
33
  db,
31
- "SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
34
+ "SELECT source_id, target_id, confidence FROM edges WHERE kind = 'calls'",
32
35
  ).all();
33
36
  }
34
37