@optave/codegraph 3.1.4 → 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 (181) hide show
  1. package/README.md +26 -70
  2. package/package.json +10 -8
  3. package/src/ast-analysis/engine.js +32 -12
  4. package/src/ast-analysis/shared.js +2 -2
  5. package/src/cli/commands/ast.js +2 -6
  6. package/src/cli/commands/audit.js +9 -10
  7. package/src/cli/commands/batch.js +4 -4
  8. package/src/cli/commands/branch-compare.js +1 -1
  9. package/src/cli/commands/build.js +1 -1
  10. package/src/cli/commands/cfg.js +3 -7
  11. package/src/cli/commands/check.js +12 -17
  12. package/src/cli/commands/children.js +3 -6
  13. package/src/cli/commands/co-change.js +5 -3
  14. package/src/cli/commands/communities.js +2 -6
  15. package/src/cli/commands/complexity.js +3 -2
  16. package/src/cli/commands/context.js +3 -7
  17. package/src/cli/commands/cycles.js +12 -8
  18. package/src/cli/commands/dataflow.js +3 -7
  19. package/src/cli/commands/deps.js +2 -6
  20. package/src/cli/commands/diff-impact.js +2 -6
  21. package/src/cli/commands/embed.js +1 -1
  22. package/src/cli/commands/export.js +34 -31
  23. package/src/cli/commands/exports.js +2 -6
  24. package/src/cli/commands/flow.js +3 -7
  25. package/src/cli/commands/fn-impact.js +3 -7
  26. package/src/cli/commands/impact.js +2 -6
  27. package/src/cli/commands/info.js +2 -2
  28. package/src/cli/commands/map.js +1 -1
  29. package/src/cli/commands/mcp.js +1 -1
  30. package/src/cli/commands/models.js +1 -1
  31. package/src/cli/commands/owners.js +1 -1
  32. package/src/cli/commands/path.js +2 -2
  33. package/src/cli/commands/plot.js +40 -31
  34. package/src/cli/commands/query.js +3 -7
  35. package/src/cli/commands/registry.js +2 -2
  36. package/src/cli/commands/roles.js +3 -7
  37. package/src/cli/commands/search.js +1 -1
  38. package/src/cli/commands/sequence.js +3 -7
  39. package/src/cli/commands/snapshot.js +6 -1
  40. package/src/cli/commands/stats.js +1 -1
  41. package/src/cli/commands/structure.js +5 -4
  42. package/src/cli/commands/triage.js +4 -4
  43. package/src/cli/commands/watch.js +1 -1
  44. package/src/cli/commands/where.js +2 -6
  45. package/src/cli/index.js +11 -5
  46. package/src/cli/shared/open-graph.js +13 -0
  47. package/src/cli/shared/options.js +22 -2
  48. package/src/cli.js +1 -1
  49. package/src/db/connection.js +127 -4
  50. package/src/{db.js → db/index.js} +12 -5
  51. package/src/db/migrations.js +1 -1
  52. package/src/db/query-builder.js +15 -8
  53. package/src/db/repository/base.js +1 -1
  54. package/src/db/repository/graph-read.js +3 -3
  55. package/src/db/repository/in-memory-repository.js +4 -13
  56. package/src/db/repository/nodes.js +3 -8
  57. package/src/{analysis → domain/analysis}/context.js +6 -6
  58. package/src/{analysis → domain/analysis}/dependencies.js +5 -5
  59. package/src/{analysis → domain/analysis}/exports.js +8 -4
  60. package/src/{analysis → domain/analysis}/impact.js +61 -58
  61. package/src/{analysis → domain/analysis}/module-map.js +3 -3
  62. package/src/{analysis → domain/analysis}/roles.js +4 -4
  63. package/src/{analysis → domain/analysis}/symbol-lookup.js +13 -7
  64. package/src/{builder → domain/graph/builder}/helpers.js +3 -3
  65. package/src/{builder → domain/graph/builder}/incremental.js +3 -3
  66. package/src/{builder → domain/graph/builder}/pipeline.js +4 -4
  67. package/src/{builder → domain/graph/builder}/stages/build-edges.js +2 -2
  68. package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
  69. package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
  70. package/src/{builder → domain/graph/builder}/stages/detect-changes.js +6 -6
  71. package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
  72. package/src/{builder → domain/graph/builder}/stages/insert-nodes.js +1 -1
  73. package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
  74. package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
  75. package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
  76. package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
  77. package/src/{cycles.js → domain/graph/cycles.js} +4 -4
  78. package/src/{journal.js → domain/graph/journal.js} +1 -1
  79. package/src/{resolve.js → domain/graph/resolve.js} +2 -2
  80. package/src/{watcher.js → domain/graph/watcher.js} +5 -5
  81. package/src/{parser.js → domain/parser.js} +5 -5
  82. package/src/{queries.js → domain/queries.js} +16 -16
  83. package/src/{embeddings → domain/search}/generator.js +3 -3
  84. package/src/{embeddings → domain/search}/models.js +2 -2
  85. package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
  86. package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
  87. package/src/{embeddings → domain/search}/search/keyword.js +1 -1
  88. package/src/{embeddings → domain/search}/search/prepare.js +2 -2
  89. package/src/{embeddings → domain/search}/search/semantic.js +1 -1
  90. package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
  91. package/src/extractors/javascript.js +1 -1
  92. package/src/{ast.js → features/ast.js} +8 -8
  93. package/src/{audit.js → features/audit.js} +16 -44
  94. package/src/{batch.js → features/batch.js} +5 -5
  95. package/src/{boundaries.js → features/boundaries.js} +2 -2
  96. package/src/{branch-compare.js → features/branch-compare.js} +3 -3
  97. package/src/{cfg.js → features/cfg.js} +10 -10
  98. package/src/{check.js → features/check.js} +13 -30
  99. package/src/{cochange.js → features/cochange.js} +5 -5
  100. package/src/{communities.js → features/communities.js} +7 -7
  101. package/src/{complexity.js → features/complexity.js} +13 -13
  102. package/src/{dataflow.js → features/dataflow.js} +11 -11
  103. package/src/{export.js → features/export.js} +3 -3
  104. package/src/{flow.js → features/flow.js} +4 -4
  105. package/src/{viewer.js → features/graph-enrichment.js} +6 -6
  106. package/src/{manifesto.js → features/manifesto.js} +6 -6
  107. package/src/{owners.js → features/owners.js} +2 -2
  108. package/src/{sequence.js → features/sequence.js} +15 -15
  109. package/src/{snapshot.js → features/snapshot.js} +3 -3
  110. package/src/{structure.js → features/structure.js} +7 -7
  111. package/src/{triage.js → features/triage.js} +8 -8
  112. package/src/graph/builders/dependency.js +33 -14
  113. package/src/index.cjs +16 -0
  114. package/src/index.js +39 -39
  115. package/src/{native.js → infrastructure/native.js} +1 -1
  116. package/src/mcp/middleware.js +1 -1
  117. package/src/mcp/server.js +5 -5
  118. package/src/mcp/tool-registry.js +2 -2
  119. package/src/mcp/tools/ast-query.js +1 -1
  120. package/src/mcp/tools/audit.js +1 -1
  121. package/src/mcp/tools/batch-query.js +1 -1
  122. package/src/mcp/tools/branch-compare.js +3 -1
  123. package/src/mcp/tools/cfg.js +1 -1
  124. package/src/mcp/tools/check.js +3 -3
  125. package/src/mcp/tools/co-changes.js +1 -1
  126. package/src/mcp/tools/code-owners.js +1 -1
  127. package/src/mcp/tools/communities.js +1 -1
  128. package/src/mcp/tools/complexity.js +1 -1
  129. package/src/mcp/tools/dataflow.js +2 -2
  130. package/src/mcp/tools/execution-flow.js +2 -2
  131. package/src/mcp/tools/export-graph.js +2 -2
  132. package/src/mcp/tools/find-cycles.js +2 -2
  133. package/src/mcp/tools/list-repos.js +1 -1
  134. package/src/mcp/tools/sequence.js +1 -1
  135. package/src/mcp/tools/structure.js +1 -1
  136. package/src/mcp/tools/triage.js +2 -2
  137. package/src/{commands → presentation}/audit.js +2 -2
  138. package/src/{commands → presentation}/batch.js +1 -1
  139. package/src/{commands → presentation}/branch-compare.js +2 -2
  140. package/src/{commands → presentation}/cfg.js +1 -1
  141. package/src/{commands → presentation}/check.js +2 -2
  142. package/src/{commands → presentation}/communities.js +1 -1
  143. package/src/{commands → presentation}/complexity.js +1 -1
  144. package/src/{commands → presentation}/dataflow.js +1 -1
  145. package/src/{commands → presentation}/flow.js +2 -2
  146. package/src/{commands → presentation}/manifesto.js +1 -1
  147. package/src/{commands → presentation}/owners.js +1 -1
  148. package/src/presentation/queries-cli/exports.js +46 -0
  149. package/src/presentation/queries-cli/impact.js +198 -0
  150. package/src/presentation/queries-cli/index.js +5 -0
  151. package/src/presentation/queries-cli/inspect.js +334 -0
  152. package/src/presentation/queries-cli/overview.js +197 -0
  153. package/src/presentation/queries-cli/path.js +58 -0
  154. package/src/presentation/queries-cli.js +27 -0
  155. package/src/{commands → presentation}/query.js +1 -1
  156. package/src/presentation/result-formatter.js +126 -3
  157. package/src/{commands → presentation}/sequence.js +2 -2
  158. package/src/{commands → presentation}/structure.js +1 -1
  159. package/src/{commands → presentation}/triage.js +1 -1
  160. package/src/{constants.js → shared/constants.js} +1 -1
  161. package/src/shared/file-utils.js +2 -2
  162. package/src/shared/generators.js +2 -2
  163. package/src/shared/hierarchy.js +1 -1
  164. package/src/mcp.js +0 -2
  165. package/src/queries-cli.js +0 -866
  166. /package/src/{builder → domain/graph/builder}/context.js +0 -0
  167. /package/src/{builder.js → domain/graph/builder.js} +0 -0
  168. /package/src/{embeddings → domain/search}/index.js +0 -0
  169. /package/src/{embeddings → domain/search}/search/filters.js +0 -0
  170. /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
  171. /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
  172. /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
  173. /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
  174. /package/src/{config.js → infrastructure/config.js} +0 -0
  175. /package/src/{logger.js → infrastructure/logger.js} +0 -0
  176. /package/src/{registry.js → infrastructure/registry.js} +0 -0
  177. /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
  178. /package/src/{commands → presentation}/cochange.js +0 -0
  179. /package/src/{errors.js → shared/errors.js} +0 -0
  180. /package/src/{kinds.js → shared/kinds.js} +0 -0
  181. /package/src/{paginate.js → shared/paginate.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { loadConfig } from '../../config.js';
1
+ import { loadConfig } from '../../infrastructure/config.js';
2
2
 
3
3
  const config = loadConfig(process.cwd());
4
4
 
@@ -15,7 +15,9 @@ export function applyQueryOpts(cmd) {
15
15
  .option('-j, --json', 'Output as JSON')
16
16
  .option('--limit <number>', 'Max results to return')
17
17
  .option('--offset <number>', 'Skip N results (default: 0)')
18
- .option('--ndjson', 'Newline-delimited JSON output');
18
+ .option('--ndjson', 'Newline-delimited JSON output')
19
+ .option('--table', 'Output as aligned table')
20
+ .option('--csv', 'Output as CSV');
19
21
  }
20
22
 
21
23
  /**
@@ -30,6 +32,24 @@ export function resolveNoTests(opts) {
30
32
  return config.query?.excludeTests || false;
31
33
  }
32
34
 
35
+ /**
36
+ * Extract the common query option fields shared by most analysis commands.
37
+ *
38
+ * Spreads cleanly into per-command option objects:
39
+ * `{ ...resolveQueryOpts(opts), depth: parseInt(opts.depth, 10) }`
40
+ */
41
+ export function resolveQueryOpts(opts) {
42
+ return {
43
+ noTests: resolveNoTests(opts),
44
+ json: opts.json,
45
+ ndjson: opts.ndjson,
46
+ table: opts.table,
47
+ csv: opts.csv,
48
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
49
+ offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
50
+ };
51
+ }
52
+
33
53
  export function formatSize(bytes) {
34
54
  if (bytes < 1024) return `${bytes} B`;
35
55
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
package/src/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { run } from './cli/index.js';
4
- import { CodegraphError } from './errors.js';
4
+ import { CodegraphError } from './shared/errors.js';
5
5
 
6
6
  run().catch((err) => {
7
7
  if (err instanceof CodegraphError) {
@@ -1,8 +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 { DbError } from '../errors.js';
5
- 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
+ }
6
58
 
7
59
  function isProcessAlive(pid) {
8
60
  try {
@@ -44,6 +96,22 @@ function releaseAdvisoryLock(lockPath) {
44
96
  }
45
97
  }
46
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
+
47
115
  export function openDb(dbPath) {
48
116
  const dir = path.dirname(dbPath);
49
117
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -62,15 +130,41 @@ export function closeDb(db) {
62
130
 
63
131
  export function findDbPath(customPath) {
64
132
  if (customPath) return path.resolve(customPath);
65
- 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
+ }
66
155
  while (true) {
67
156
  const candidate = path.join(dir, '.codegraph', 'graph.db');
68
157
  if (fs.existsSync(candidate)) return candidate;
158
+ if (ceiling && isSameDirectory(dir, ceiling)) {
159
+ debug(`findDbPath: stopped at git ceiling ${ceiling}`);
160
+ break;
161
+ }
69
162
  const parent = path.dirname(dir);
70
163
  if (parent === dir) break;
71
164
  dir = parent;
72
165
  }
73
- return path.join(process.cwd(), '.codegraph', 'graph.db');
166
+ const base = ceiling || process.cwd();
167
+ return path.join(base, '.codegraph', 'graph.db');
74
168
  }
75
169
 
76
170
  /**
@@ -86,3 +180,32 @@ export function openReadonlyOrFail(customPath) {
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,
@@ -60,4 +67,4 @@ export {
60
67
  Repository,
61
68
  SqliteRepository,
62
69
  upsertCoChangeMeta,
63
- } 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 = [
@@ -1,5 +1,5 @@
1
- import { DbError } from '../errors.js';
2
- import { EVERY_EDGE_KIND } from '../kinds.js';
1
+ import { DbError } from '../shared/errors.js';
2
+ import { EVERY_EDGE_KIND } from '../shared/kinds.js';
3
3
 
4
4
  // ─── Validation Helpers ─────────────────────────────────────────────
5
5
 
@@ -65,6 +65,13 @@ function validateEdgeKind(edgeKind) {
65
65
  }
66
66
  }
67
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
+
68
75
  // ─── Standalone Helpers ──────────────────────────────────────────────
69
76
 
70
77
  /**
@@ -164,11 +171,11 @@ export class NodeQuery {
164
171
  return this;
165
172
  }
166
173
 
167
- /** WHERE n.file LIKE ? (no-op if falsy). */
174
+ /** WHERE n.file LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
168
175
  fileFilter(file) {
169
176
  if (!file) return this;
170
- this.#conditions.push('n.file LIKE ?');
171
- this.#params.push(`%${file}%`);
177
+ this.#conditions.push("n.file LIKE ? ESCAPE '\\'");
178
+ this.#params.push(`%${escapeLike(file)}%`);
172
179
  return this;
173
180
  }
174
181
 
@@ -188,11 +195,11 @@ export class NodeQuery {
188
195
  return this;
189
196
  }
190
197
 
191
- /** WHERE n.name LIKE ? (no-op if falsy). */
198
+ /** WHERE n.name LIKE ? (no-op if falsy). Escapes LIKE wildcards in the value. */
192
199
  nameLike(pattern) {
193
200
  if (!pattern) return this;
194
- this.#conditions.push('n.name LIKE ?');
195
- this.#params.push(`%${pattern}%`);
201
+ this.#conditions.push("n.name LIKE ? ESCAPE '\\'");
202
+ this.#params.push(`%${escapeLike(pattern)}%`);
196
203
  return this;
197
204
  }
198
205
 
@@ -163,7 +163,7 @@ export class Repository {
163
163
  throw new Error('not implemented');
164
164
  }
165
165
 
166
- /** @returns {{ source_id: number, target_id: number }[]} */
166
+ /** @returns {{ source_id: number, target_id: number, confidence: number|null }[]} */
167
167
  getCallEdges() {
168
168
  throw new Error('not implemented');
169
169
  }
@@ -1,4 +1,4 @@
1
- import { CORE_SYMBOL_KINDS } from '../../kinds.js';
1
+ import { CORE_SYMBOL_KINDS } from '../../shared/kinds.js';
2
2
  import { cachedStmt } from './cached-stmt.js';
3
3
 
4
4
  // ─── Statement caches (one prepared statement per db instance) ────────────
@@ -25,13 +25,13 @@ export function getCallableNodes(db) {
25
25
  /**
26
26
  * Get all 'calls' edges.
27
27
  * @param {object} db
28
- * @returns {{ source_id: number, target_id: number }[]}
28
+ * @returns {{ source_id: number, target_id: number, confidence: number|null }[]}
29
29
  */
30
30
  export function getCallEdges(db) {
31
31
  return cachedStmt(
32
32
  _getCallEdgesStmt,
33
33
  db,
34
- "SELECT source_id, target_id FROM edges WHERE kind = 'calls'",
34
+ "SELECT source_id, target_id, confidence FROM edges WHERE kind = 'calls'",
35
35
  ).all();
36
36
  }
37
37
 
@@ -1,17 +1,8 @@
1
- import { ConfigError } from '../../errors.js';
2
- import { CORE_SYMBOL_KINDS, EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
1
+ import { ConfigError } from '../../shared/errors.js';
2
+ import { CORE_SYMBOL_KINDS, EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js';
3
+ import { escapeLike } from '../query-builder.js';
3
4
  import { Repository } from './base.js';
4
5
 
5
- /**
6
- * Escape LIKE special characters so they are treated as literals.
7
- * Mirrors the `escapeLike` function in `nodes.js`.
8
- * @param {string} s
9
- * @returns {string}
10
- */
11
- function escapeLike(s) {
12
- return s.replace(/[%_\\]/g, '\\$&');
13
- }
14
-
15
6
  /**
16
7
  * Convert a SQL LIKE pattern to a RegExp (case-insensitive).
17
8
  * Supports `%` (any chars) and `_` (single char).
@@ -498,7 +489,7 @@ export class InMemoryRepository extends Repository {
498
489
  getCallEdges() {
499
490
  return [...this.#edges.values()]
500
491
  .filter((e) => e.kind === 'calls')
501
- .map((e) => ({ source_id: e.source_id, target_id: e.target_id }));
492
+ .map((e) => ({ source_id: e.source_id, target_id: e.target_id, confidence: e.confidence }));
502
493
  }
503
494
 
504
495
  getFileNodesAll() {
@@ -1,6 +1,6 @@
1
- import { ConfigError } from '../../errors.js';
2
- import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js';
3
- import { NodeQuery } from '../query-builder.js';
1
+ import { ConfigError } from '../../shared/errors.js';
2
+ import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js';
3
+ import { escapeLike, NodeQuery } from '../query-builder.js';
4
4
  import { cachedStmt } from './cached-stmt.js';
5
5
 
6
6
  // ─── Query-builder based lookups (moved from src/db/repository.js) ─────
@@ -250,11 +250,6 @@ export function findNodeChildren(db, parentId) {
250
250
  ).all(parentId);
251
251
  }
252
252
 
253
- /** Escape LIKE wildcards in a literal string segment. */
254
- function escapeLike(s) {
255
- return s.replace(/[%_\\]/g, '\\$&');
256
- }
257
-
258
253
  /**
259
254
  * Find all nodes that belong to a given scope (by scope column).
260
255
  * Enables "all methods of class X" without traversing edges.
@@ -12,18 +12,18 @@ import {
12
12
  findNodesByFile,
13
13
  getComplexityForNode,
14
14
  openReadonlyOrFail,
15
- } from '../db.js';
16
- import { isTestFile } from '../infrastructure/test-filter.js';
17
- import { paginateResult } from '../paginate.js';
15
+ } from '../../db/index.js';
16
+ import { isTestFile } from '../../infrastructure/test-filter.js';
18
17
  import {
19
18
  createFileLinesReader,
20
19
  extractSignature,
21
20
  extractSummary,
22
21
  isFileLikeTarget,
23
22
  readSourceRange,
24
- } from '../shared/file-utils.js';
25
- import { resolveMethodViaHierarchy } from '../shared/hierarchy.js';
26
- import { normalizeSymbol } from '../shared/normalize.js';
23
+ } from '../../shared/file-utils.js';
24
+ import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
25
+ import { normalizeSymbol } from '../../shared/normalize.js';
26
+ import { paginateResult } from '../../shared/paginate.js';
27
27
  import { findMatchingNodes } from './symbol-lookup.js';
28
28
 
29
29
  function explainFileImpl(db, target, getFileLines) {
@@ -6,11 +6,11 @@ import {
6
6
  findImportTargets,
7
7
  findNodesByFile,
8
8
  openReadonlyOrFail,
9
- } from '../db.js';
10
- import { isTestFile } from '../infrastructure/test-filter.js';
11
- import { paginateResult } from '../paginate.js';
12
- import { resolveMethodViaHierarchy } from '../shared/hierarchy.js';
13
- import { normalizeSymbol } from '../shared/normalize.js';
9
+ } from '../../db/index.js';
10
+ import { isTestFile } from '../../infrastructure/test-filter.js';
11
+ import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
12
+ import { normalizeSymbol } from '../../shared/normalize.js';
13
+ import { paginateResult } from '../../shared/paginate.js';
14
14
  import { findMatchingNodes } from './symbol-lookup.js';
15
15
 
16
16
  export function fileDepsData(file, customDbPath, opts = {}) {
@@ -5,10 +5,14 @@ import {
5
5
  findFileNodes,
6
6
  findNodesByFile,
7
7
  openReadonlyOrFail,
8
- } from '../db.js';
9
- import { isTestFile } from '../infrastructure/test-filter.js';
10
- import { paginateResult } from '../paginate.js';
11
- import { createFileLinesReader, extractSignature, extractSummary } from '../shared/file-utils.js';
8
+ } from '../../db/index.js';
9
+ import { isTestFile } from '../../infrastructure/test-filter.js';
10
+ import {
11
+ createFileLinesReader,
12
+ extractSignature,
13
+ extractSummary,
14
+ } from '../../shared/file-utils.js';
15
+ import { paginateResult } from '../../shared/paginate.js';
12
16
 
13
17
  export function exportsData(file, customDbPath, opts = {}) {
14
18
  const db = openReadonlyOrFail(customDbPath);
@@ -1,9 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { evaluateBoundaries } from '../boundaries.js';
5
- import { coChangeForFiles } from '../cochange.js';
6
- import { loadConfig } from '../config.js';
7
4
  import {
8
5
  findDbPath,
9
6
  findDistinctCallers,
@@ -11,13 +8,52 @@ import {
11
8
  findImportDependents,
12
9
  findNodeById,
13
10
  openReadonlyOrFail,
14
- } from '../db.js';
15
- import { isTestFile } from '../infrastructure/test-filter.js';
16
- import { ownersForFiles } from '../owners.js';
17
- import { paginateResult } from '../paginate.js';
18
- import { normalizeSymbol } from '../shared/normalize.js';
11
+ } from '../../db/index.js';
12
+ import { evaluateBoundaries } from '../../features/boundaries.js';
13
+ import { coChangeForFiles } from '../../features/cochange.js';
14
+ import { ownersForFiles } from '../../features/owners.js';
15
+ import { loadConfig } from '../../infrastructure/config.js';
16
+ import { isTestFile } from '../../infrastructure/test-filter.js';
17
+ import { normalizeSymbol } from '../../shared/normalize.js';
18
+ import { paginateResult } from '../../shared/paginate.js';
19
19
  import { findMatchingNodes } from './symbol-lookup.js';
20
20
 
21
+ // ─── Shared BFS: transitive callers ────────────────────────────────────
22
+
23
+ /**
24
+ * BFS traversal to find transitive callers of a node.
25
+ *
26
+ * @param {import('better-sqlite3').Database} db - Open read-only SQLite database handle (not a Repository)
27
+ * @param {number} startId - Starting node ID
28
+ * @param {{ noTests?: boolean, maxDepth?: number, onVisit?: (caller: object, parentId: number, depth: number) => void }} options
29
+ * @returns {{ totalDependents: number, levels: Record<number, Array<{name:string, kind:string, file:string, line:number}>> }}
30
+ */
31
+ export function bfsTransitiveCallers(db, startId, { noTests = false, maxDepth = 3, onVisit } = {}) {
32
+ const visited = new Set([startId]);
33
+ const levels = {};
34
+ let frontier = [startId];
35
+
36
+ for (let d = 1; d <= maxDepth; d++) {
37
+ const nextFrontier = [];
38
+ for (const fid of frontier) {
39
+ const callers = findDistinctCallers(db, fid);
40
+ for (const c of callers) {
41
+ if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
42
+ visited.add(c.id);
43
+ nextFrontier.push(c.id);
44
+ if (!levels[d]) levels[d] = [];
45
+ levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
46
+ if (onVisit) onVisit(c, fid, d);
47
+ }
48
+ }
49
+ }
50
+ frontier = nextFrontier;
51
+ if (frontier.length === 0) break;
52
+ }
53
+
54
+ return { totalDependents: visited.size - 1, levels };
55
+ }
56
+
21
57
  export function impactAnalysisData(file, customDbPath, opts = {}) {
22
58
  const db = openReadonlyOrFail(customDbPath);
23
59
  try {
@@ -82,31 +118,11 @@ export function fnImpactData(name, customDbPath, opts = {}) {
82
118
  }
83
119
 
84
120
  const results = nodes.map((node) => {
85
- const visited = new Set([node.id]);
86
- const levels = {};
87
- let frontier = [node.id];
88
-
89
- for (let d = 1; d <= maxDepth; d++) {
90
- const nextFrontier = [];
91
- for (const fid of frontier) {
92
- const callers = findDistinctCallers(db, fid);
93
- for (const c of callers) {
94
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
95
- visited.add(c.id);
96
- nextFrontier.push(c.id);
97
- if (!levels[d]) levels[d] = [];
98
- levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
99
- }
100
- }
101
- }
102
- frontier = nextFrontier;
103
- if (frontier.length === 0) break;
104
- }
105
-
121
+ const { levels, totalDependents } = bfsTransitiveCallers(db, node.id, { noTests, maxDepth });
106
122
  return {
107
123
  ...normalizeSymbol(node, db, hc),
108
124
  levels,
109
- totalDependents: visited.size - 1,
125
+ totalDependents,
110
126
  };
111
127
  });
112
128
 
@@ -232,40 +248,27 @@ export function diffImpactData(customDbPath, opts = {}) {
232
248
 
233
249
  const allAffected = new Set();
234
250
  const functionResults = affectedFunctions.map((fn) => {
235
- const visited = new Set([fn.id]);
236
- let frontier = [fn.id];
237
- let totalCallers = 0;
238
- const levels = {};
239
251
  const edges = [];
240
252
  const idToKey = new Map();
241
253
  idToKey.set(fn.id, `${fn.file}::${fn.name}:${fn.line}`);
242
- for (let d = 1; d <= maxDepth; d++) {
243
- const nextFrontier = [];
244
- for (const fid of frontier) {
245
- const callers = findDistinctCallers(db, fid);
246
- for (const c of callers) {
247
- if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
248
- visited.add(c.id);
249
- nextFrontier.push(c.id);
250
- allAffected.add(`${c.file}:${c.name}`);
251
- const callerKey = `${c.file}::${c.name}:${c.line}`;
252
- idToKey.set(c.id, callerKey);
253
- if (!levels[d]) levels[d] = [];
254
- levels[d].push({ name: c.name, kind: c.kind, file: c.file, line: c.line });
255
- edges.push({ from: idToKey.get(fid), to: callerKey });
256
- totalCallers++;
257
- }
258
- }
259
- }
260
- frontier = nextFrontier;
261
- if (frontier.length === 0) break;
262
- }
254
+
255
+ const { levels, totalDependents } = bfsTransitiveCallers(db, fn.id, {
256
+ noTests,
257
+ maxDepth,
258
+ onVisit(c, parentId) {
259
+ allAffected.add(`${c.file}:${c.name}`);
260
+ const callerKey = `${c.file}::${c.name}:${c.line}`;
261
+ idToKey.set(c.id, callerKey);
262
+ edges.push({ from: idToKey.get(parentId), to: callerKey });
263
+ },
264
+ });
265
+
263
266
  return {
264
267
  name: fn.name,
265
268
  kind: fn.kind,
266
269
  file: fn.file,
267
270
  line: fn.line,
268
- transitiveCallers: totalCallers,
271
+ transitiveCallers: totalDependents,
269
272
  levels,
270
273
  edges,
271
274
  };
@@ -310,8 +313,8 @@ export function diffImpactData(customDbPath, opts = {}) {
310
313
  let boundaryViolations = [];
311
314
  let boundaryViolationCount = 0;
312
315
  try {
313
- const config = loadConfig(repoRoot);
314
- const boundaryConfig = config.manifesto?.boundaries;
316
+ const cfg = opts.config || loadConfig(repoRoot);
317
+ const boundaryConfig = cfg.manifesto?.boundaries;
315
318
  if (boundaryConfig) {
316
319
  const result = evaluateBoundaries(db, boundaryConfig, {
317
320
  scopeFiles: [...changedRanges.keys()],
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
- import { findCycles } from '../cycles.js';
3
- import { openReadonlyOrFail, testFilterSQL } from '../db.js';
4
- import { isTestFile } from '../infrastructure/test-filter.js';
2
+ import { openReadonlyOrFail, testFilterSQL } from '../../db/index.js';
3
+ import { isTestFile } from '../../infrastructure/test-filter.js';
4
+ import { findCycles } from '../graph/cycles.js';
5
5
  import { LANGUAGE_REGISTRY } from '../parser.js';
6
6
 
7
7
  export const FALSE_POSITIVE_NAMES = new Set([
@@ -1,7 +1,7 @@
1
- import { openReadonlyOrFail } from '../db.js';
2
- import { isTestFile } from '../infrastructure/test-filter.js';
3
- import { paginateResult } from '../paginate.js';
4
- import { normalizeSymbol } from '../shared/normalize.js';
1
+ import { openReadonlyOrFail } from '../../db/index.js';
2
+ import { isTestFile } from '../../infrastructure/test-filter.js';
3
+ import { normalizeSymbol } from '../../shared/normalize.js';
4
+ import { paginateResult } from '../../shared/paginate.js';
5
5
 
6
6
  export function rolesData(customDbPath, opts = {}) {
7
7
  const db = openReadonlyOrFail(customDbPath);