@optave/codegraph 3.1.3 → 3.1.4

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 (185) hide show
  1. package/README.md +17 -19
  2. package/package.json +10 -7
  3. package/src/analysis/context.js +408 -0
  4. package/src/analysis/dependencies.js +341 -0
  5. package/src/analysis/exports.js +130 -0
  6. package/src/analysis/impact.js +463 -0
  7. package/src/analysis/module-map.js +322 -0
  8. package/src/analysis/roles.js +45 -0
  9. package/src/analysis/symbol-lookup.js +232 -0
  10. package/src/ast-analysis/shared.js +5 -4
  11. package/src/batch.js +2 -1
  12. package/src/builder/context.js +85 -0
  13. package/src/builder/helpers.js +218 -0
  14. package/src/builder/incremental.js +178 -0
  15. package/src/builder/pipeline.js +130 -0
  16. package/src/builder/stages/build-edges.js +297 -0
  17. package/src/builder/stages/build-structure.js +113 -0
  18. package/src/builder/stages/collect-files.js +44 -0
  19. package/src/builder/stages/detect-changes.js +413 -0
  20. package/src/builder/stages/finalize.js +139 -0
  21. package/src/builder/stages/insert-nodes.js +195 -0
  22. package/src/builder/stages/parse-files.js +28 -0
  23. package/src/builder/stages/resolve-imports.js +143 -0
  24. package/src/builder/stages/run-analyses.js +44 -0
  25. package/src/builder.js +10 -1485
  26. package/src/cfg.js +1 -2
  27. package/src/cli/commands/ast.js +26 -0
  28. package/src/cli/commands/audit.js +46 -0
  29. package/src/cli/commands/batch.js +68 -0
  30. package/src/cli/commands/branch-compare.js +21 -0
  31. package/src/cli/commands/build.js +26 -0
  32. package/src/cli/commands/cfg.js +30 -0
  33. package/src/cli/commands/check.js +79 -0
  34. package/src/cli/commands/children.js +31 -0
  35. package/src/cli/commands/co-change.js +65 -0
  36. package/src/cli/commands/communities.js +23 -0
  37. package/src/cli/commands/complexity.js +45 -0
  38. package/src/cli/commands/context.js +34 -0
  39. package/src/cli/commands/cycles.js +28 -0
  40. package/src/cli/commands/dataflow.js +32 -0
  41. package/src/cli/commands/deps.js +16 -0
  42. package/src/cli/commands/diff-impact.js +30 -0
  43. package/src/cli/commands/embed.js +30 -0
  44. package/src/cli/commands/export.js +75 -0
  45. package/src/cli/commands/exports.js +18 -0
  46. package/src/cli/commands/flow.js +36 -0
  47. package/src/cli/commands/fn-impact.js +30 -0
  48. package/src/cli/commands/impact.js +16 -0
  49. package/src/cli/commands/info.js +76 -0
  50. package/src/cli/commands/map.js +19 -0
  51. package/src/cli/commands/mcp.js +18 -0
  52. package/src/cli/commands/models.js +19 -0
  53. package/src/cli/commands/owners.js +25 -0
  54. package/src/cli/commands/path.js +36 -0
  55. package/src/cli/commands/plot.js +80 -0
  56. package/src/cli/commands/query.js +49 -0
  57. package/src/cli/commands/registry.js +100 -0
  58. package/src/cli/commands/roles.js +34 -0
  59. package/src/cli/commands/search.js +42 -0
  60. package/src/cli/commands/sequence.js +32 -0
  61. package/src/cli/commands/snapshot.js +61 -0
  62. package/src/cli/commands/stats.js +15 -0
  63. package/src/cli/commands/structure.js +32 -0
  64. package/src/cli/commands/triage.js +78 -0
  65. package/src/cli/commands/watch.js +12 -0
  66. package/src/cli/commands/where.js +24 -0
  67. package/src/cli/index.js +118 -0
  68. package/src/cli/shared/options.js +39 -0
  69. package/src/cli/shared/output.js +1 -0
  70. package/src/cli.js +11 -1522
  71. package/src/commands/check.js +5 -5
  72. package/src/commands/manifesto.js +3 -3
  73. package/src/commands/structure.js +1 -1
  74. package/src/communities.js +15 -87
  75. package/src/cycles.js +30 -85
  76. package/src/dataflow.js +1 -2
  77. package/src/db/connection.js +4 -4
  78. package/src/db/migrations.js +41 -0
  79. package/src/db/query-builder.js +6 -5
  80. package/src/db/repository/base.js +201 -0
  81. package/src/db/repository/graph-read.js +5 -2
  82. package/src/db/repository/in-memory-repository.js +584 -0
  83. package/src/db/repository/index.js +5 -1
  84. package/src/db/repository/nodes.js +63 -4
  85. package/src/db/repository/sqlite-repository.js +219 -0
  86. package/src/db.js +5 -0
  87. package/src/embeddings/generator.js +163 -0
  88. package/src/embeddings/index.js +13 -0
  89. package/src/embeddings/models.js +218 -0
  90. package/src/embeddings/search/cli-formatter.js +151 -0
  91. package/src/embeddings/search/filters.js +46 -0
  92. package/src/embeddings/search/hybrid.js +121 -0
  93. package/src/embeddings/search/keyword.js +68 -0
  94. package/src/embeddings/search/prepare.js +66 -0
  95. package/src/embeddings/search/semantic.js +145 -0
  96. package/src/embeddings/stores/fts5.js +27 -0
  97. package/src/embeddings/stores/sqlite-blob.js +24 -0
  98. package/src/embeddings/strategies/source.js +14 -0
  99. package/src/embeddings/strategies/structured.js +43 -0
  100. package/src/embeddings/strategies/text-utils.js +43 -0
  101. package/src/errors.js +78 -0
  102. package/src/export.js +217 -520
  103. package/src/extractors/csharp.js +10 -2
  104. package/src/extractors/go.js +3 -1
  105. package/src/extractors/helpers.js +71 -0
  106. package/src/extractors/java.js +9 -2
  107. package/src/extractors/javascript.js +38 -1
  108. package/src/extractors/php.js +3 -1
  109. package/src/extractors/python.js +14 -3
  110. package/src/extractors/rust.js +3 -1
  111. package/src/graph/algorithms/bfs.js +49 -0
  112. package/src/graph/algorithms/centrality.js +16 -0
  113. package/src/graph/algorithms/index.js +5 -0
  114. package/src/graph/algorithms/louvain.js +26 -0
  115. package/src/graph/algorithms/shortest-path.js +41 -0
  116. package/src/graph/algorithms/tarjan.js +49 -0
  117. package/src/graph/builders/dependency.js +91 -0
  118. package/src/graph/builders/index.js +3 -0
  119. package/src/graph/builders/structure.js +40 -0
  120. package/src/graph/builders/temporal.js +33 -0
  121. package/src/graph/classifiers/index.js +2 -0
  122. package/src/graph/classifiers/risk.js +85 -0
  123. package/src/graph/classifiers/roles.js +64 -0
  124. package/src/graph/index.js +13 -0
  125. package/src/graph/model.js +230 -0
  126. package/src/index.js +33 -210
  127. package/src/infrastructure/result-formatter.js +2 -21
  128. package/src/mcp/index.js +2 -0
  129. package/src/mcp/middleware.js +26 -0
  130. package/src/mcp/server.js +128 -0
  131. package/src/mcp/tool-registry.js +801 -0
  132. package/src/mcp/tools/ast-query.js +14 -0
  133. package/src/mcp/tools/audit.js +21 -0
  134. package/src/mcp/tools/batch-query.js +11 -0
  135. package/src/mcp/tools/branch-compare.js +10 -0
  136. package/src/mcp/tools/cfg.js +21 -0
  137. package/src/mcp/tools/check.js +43 -0
  138. package/src/mcp/tools/co-changes.js +20 -0
  139. package/src/mcp/tools/code-owners.js +12 -0
  140. package/src/mcp/tools/communities.js +15 -0
  141. package/src/mcp/tools/complexity.js +18 -0
  142. package/src/mcp/tools/context.js +17 -0
  143. package/src/mcp/tools/dataflow.js +26 -0
  144. package/src/mcp/tools/diff-impact.js +24 -0
  145. package/src/mcp/tools/execution-flow.js +26 -0
  146. package/src/mcp/tools/export-graph.js +57 -0
  147. package/src/mcp/tools/file-deps.js +12 -0
  148. package/src/mcp/tools/file-exports.js +13 -0
  149. package/src/mcp/tools/find-cycles.js +15 -0
  150. package/src/mcp/tools/fn-impact.js +15 -0
  151. package/src/mcp/tools/impact-analysis.js +12 -0
  152. package/src/mcp/tools/index.js +71 -0
  153. package/src/mcp/tools/list-functions.js +14 -0
  154. package/src/mcp/tools/list-repos.js +11 -0
  155. package/src/mcp/tools/module-map.js +6 -0
  156. package/src/mcp/tools/node-roles.js +14 -0
  157. package/src/mcp/tools/path.js +12 -0
  158. package/src/mcp/tools/query.js +30 -0
  159. package/src/mcp/tools/semantic-search.js +65 -0
  160. package/src/mcp/tools/sequence.js +17 -0
  161. package/src/mcp/tools/structure.js +15 -0
  162. package/src/mcp/tools/symbol-children.js +14 -0
  163. package/src/mcp/tools/triage.js +35 -0
  164. package/src/mcp/tools/where.js +13 -0
  165. package/src/mcp.js +2 -1470
  166. package/src/native.js +3 -1
  167. package/src/presentation/colors.js +44 -0
  168. package/src/presentation/export.js +444 -0
  169. package/src/presentation/result-formatter.js +21 -0
  170. package/src/presentation/sequence-renderer.js +43 -0
  171. package/src/presentation/table.js +47 -0
  172. package/src/presentation/viewer.js +634 -0
  173. package/src/queries.js +35 -2276
  174. package/src/resolve.js +1 -1
  175. package/src/sequence.js +2 -38
  176. package/src/shared/file-utils.js +153 -0
  177. package/src/shared/generators.js +125 -0
  178. package/src/shared/hierarchy.js +27 -0
  179. package/src/shared/normalize.js +59 -0
  180. package/src/snapshot.js +6 -5
  181. package/src/structure.js +15 -40
  182. package/src/triage.js +20 -72
  183. package/src/viewer.js +35 -656
  184. package/src/watcher.js +8 -148
  185. package/src/embedder.js +0 -1097
@@ -0,0 +1,85 @@
1
+ /**
2
+ * PipelineContext — shared mutable state threaded through all build stages.
3
+ *
4
+ * Each stage reads what it needs and writes what it produces.
5
+ * This replaces the closure-captured locals in the old monolithic buildGraph().
6
+ */
7
+ export class PipelineContext {
8
+ // ── Inputs (set during setup) ──────────────────────────────────────
9
+ /** @type {string} Absolute root directory */
10
+ rootDir;
11
+ /** @type {import('better-sqlite3').Database} */
12
+ db;
13
+ /** @type {string} Absolute path to the database file */
14
+ dbPath;
15
+ /** @type {object} From loadConfig() */
16
+ config;
17
+ /** @type {object} Original buildGraph opts */
18
+ opts;
19
+ /** @type {{ engine: string, dataflow: boolean, ast: boolean }} */
20
+ engineOpts;
21
+ /** @type {string} 'native' | 'wasm' */
22
+ engineName;
23
+ /** @type {string|null} */
24
+ engineVersion;
25
+ /** @type {{ baseUrl: string|null, paths: object }} */
26
+ aliases;
27
+ /** @type {boolean} Whether incremental mode is enabled */
28
+ incremental;
29
+ /** @type {boolean} Force full rebuild (engine/schema mismatch) */
30
+ forceFullRebuild = false;
31
+ /** @type {number} Current schema version */
32
+ schemaVersion;
33
+
34
+ // ── File collection (set by collectFiles stage) ────────────────────
35
+ /** @type {string[]} Absolute file paths */
36
+ allFiles;
37
+ /** @type {Set<string>} Absolute directory paths */
38
+ discoveredDirs;
39
+
40
+ // ── Change detection (set by detectChanges stage) ──────────────────
41
+ /** @type {boolean} */
42
+ isFullBuild;
43
+ /** @type {Array<{ file: string, relPath?: string, content?: string, hash?: string, stat?: object, _reverseDepOnly?: boolean }>} */
44
+ parseChanges;
45
+ /** @type {Array<{ relPath: string, hash: string, stat: object }>} Metadata-only self-heal updates */
46
+ metadataUpdates;
47
+ /** @type {string[]} Relative paths of deleted files */
48
+ removed;
49
+ /** @type {boolean} True when no changes detected — skip remaining stages */
50
+ earlyExit = false;
51
+
52
+ // ── Parsing (set by parseFiles stage) ──────────────────────────────
53
+ /** @type {Map<string, object>} relPath → symbols from parseFilesAuto */
54
+ allSymbols;
55
+ /** @type {Map<string, object>} relPath → symbols (includes incrementally loaded) */
56
+ fileSymbols;
57
+ /** @type {Array<{ file: string, relPath?: string }>} Files to parse this build */
58
+ filesToParse;
59
+
60
+ // ── Import resolution (set by resolveImports stage) ────────────────
61
+ /** @type {Map<string, string>|null} "absFile|source" → resolved path */
62
+ batchResolved;
63
+ /** @type {Map<string, Array>} relPath → re-export descriptors */
64
+ reexportMap;
65
+ /** @type {Set<string>} Files loaded only for barrel resolution (don't rebuild edges) */
66
+ barrelOnlyFiles;
67
+
68
+ // ── Node lookup (set by insertNodes / buildEdges stages) ───────────
69
+ /** @type {Map<string, Array>} name → node rows */
70
+ nodesByName;
71
+ /** @type {Map<string, Array>} "name|file" → node rows */
72
+ nodesByNameAndFile;
73
+
74
+ // ── Misc state ─────────────────────────────────────────────────────
75
+ /** @type {boolean} Whether embeddings table exists */
76
+ hasEmbeddings = false;
77
+ /** @type {Map<string, number>} relPath → line count */
78
+ lineCountMap;
79
+
80
+ // ── Phase timing ───────────────────────────────────────────────────
81
+ timing = {};
82
+
83
+ /** @type {number} performance.now() at build start */
84
+ buildStart;
85
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Builder helper functions — shared utilities used across pipeline stages.
3
+ *
4
+ * Extracted from the monolithic builder.js so stages can import individually.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { EXTENSIONS, IGNORE_DIRS } from '../constants.js';
10
+ import { purgeFilesData } from '../db.js';
11
+ import { warn } from '../logger.js';
12
+
13
+ export const BUILTIN_RECEIVERS = new Set([
14
+ 'console',
15
+ 'Math',
16
+ 'JSON',
17
+ 'Object',
18
+ 'Array',
19
+ 'String',
20
+ 'Number',
21
+ 'Boolean',
22
+ 'Date',
23
+ 'RegExp',
24
+ 'Map',
25
+ 'Set',
26
+ 'WeakMap',
27
+ 'WeakSet',
28
+ 'Promise',
29
+ 'Symbol',
30
+ 'Error',
31
+ 'TypeError',
32
+ 'RangeError',
33
+ 'Proxy',
34
+ 'Reflect',
35
+ 'Intl',
36
+ 'globalThis',
37
+ 'window',
38
+ 'document',
39
+ 'process',
40
+ 'Buffer',
41
+ 'require',
42
+ ]);
43
+
44
+ /**
45
+ * Recursively collect all source files under `dir`.
46
+ * When `directories` is a Set, also tracks which directories contain files.
47
+ */
48
+ export function collectFiles(
49
+ dir,
50
+ files = [],
51
+ config = {},
52
+ directories = null,
53
+ _visited = new Set(),
54
+ ) {
55
+ const trackDirs = directories instanceof Set;
56
+ let hasFiles = false;
57
+
58
+ // Merge config ignoreDirs with defaults
59
+ const extraIgnore = config.ignoreDirs ? new Set(config.ignoreDirs) : null;
60
+
61
+ // Detect symlink loops (before I/O to avoid wasted readdirSync)
62
+ let realDir;
63
+ try {
64
+ realDir = fs.realpathSync(dir);
65
+ } catch {
66
+ return trackDirs ? { files, directories } : files;
67
+ }
68
+ if (_visited.has(realDir)) {
69
+ warn(`Symlink loop detected, skipping: ${dir}`);
70
+ return trackDirs ? { files, directories } : files;
71
+ }
72
+ _visited.add(realDir);
73
+
74
+ let entries;
75
+ try {
76
+ entries = fs.readdirSync(dir, { withFileTypes: true });
77
+ } catch (err) {
78
+ warn(`Cannot read directory ${dir}: ${err.message}`);
79
+ return trackDirs ? { files, directories } : files;
80
+ }
81
+
82
+ for (const entry of entries) {
83
+ if (entry.name.startsWith('.') && entry.name !== '.') {
84
+ if (IGNORE_DIRS.has(entry.name)) continue;
85
+ if (entry.isDirectory()) continue;
86
+ }
87
+ if (IGNORE_DIRS.has(entry.name)) continue;
88
+ if (extraIgnore?.has(entry.name)) continue;
89
+
90
+ const full = path.join(dir, entry.name);
91
+ if (entry.isDirectory()) {
92
+ collectFiles(full, files, config, directories, _visited);
93
+ } else if (EXTENSIONS.has(path.extname(entry.name))) {
94
+ files.push(full);
95
+ hasFiles = true;
96
+ }
97
+ }
98
+ if (trackDirs && hasFiles) {
99
+ directories.add(dir);
100
+ }
101
+ return trackDirs ? { files, directories } : files;
102
+ }
103
+
104
+ /**
105
+ * Load path aliases from tsconfig.json / jsconfig.json.
106
+ */
107
+ export function loadPathAliases(rootDir) {
108
+ const aliases = { baseUrl: null, paths: {} };
109
+ for (const configName of ['tsconfig.json', 'jsconfig.json']) {
110
+ const configPath = path.join(rootDir, configName);
111
+ if (!fs.existsSync(configPath)) continue;
112
+ try {
113
+ const raw = fs
114
+ .readFileSync(configPath, 'utf-8')
115
+ .replace(/\/\/.*$/gm, '')
116
+ .replace(/\/\*[\s\S]*?\*\//g, '')
117
+ .replace(/,\s*([\]}])/g, '$1');
118
+ const config = JSON.parse(raw);
119
+ const opts = config.compilerOptions || {};
120
+ if (opts.baseUrl) aliases.baseUrl = path.resolve(rootDir, opts.baseUrl);
121
+ if (opts.paths) {
122
+ for (const [pattern, targets] of Object.entries(opts.paths)) {
123
+ aliases.paths[pattern] = targets.map((t) => path.resolve(aliases.baseUrl || rootDir, t));
124
+ }
125
+ }
126
+ break;
127
+ } catch (err) {
128
+ warn(`Failed to parse ${configName}: ${err.message}`);
129
+ }
130
+ }
131
+ return aliases;
132
+ }
133
+
134
+ /**
135
+ * Compute MD5 hash of file contents for incremental builds.
136
+ */
137
+ export function fileHash(content) {
138
+ return createHash('md5').update(content).digest('hex');
139
+ }
140
+
141
+ /**
142
+ * Stat a file, returning { mtimeMs, size } or null on error.
143
+ */
144
+ export function fileStat(filePath) {
145
+ try {
146
+ const s = fs.statSync(filePath);
147
+ return { mtimeMs: s.mtimeMs, size: s.size };
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Read a file with retry on transient errors (EBUSY/EACCES/EPERM).
155
+ */
156
+ const TRANSIENT_CODES = new Set(['EBUSY', 'EACCES', 'EPERM']);
157
+ const RETRY_DELAY_MS = 50;
158
+
159
+ export function readFileSafe(filePath, retries = 2) {
160
+ for (let attempt = 0; ; attempt++) {
161
+ try {
162
+ return fs.readFileSync(filePath, 'utf-8');
163
+ } catch (err) {
164
+ if (attempt < retries && TRANSIENT_CODES.has(err.code)) {
165
+ const end = Date.now() + RETRY_DELAY_MS;
166
+ while (Date.now() < end) {}
167
+ continue;
168
+ }
169
+ throw err;
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Purge all graph data for the specified files.
176
+ */
177
+ export function purgeFilesFromGraph(db, files, options = {}) {
178
+ purgeFilesData(db, files, options);
179
+ }
180
+
181
+ /** Batch INSERT chunk size for multi-value INSERTs. */
182
+ export const BATCH_CHUNK = 200;
183
+
184
+ /**
185
+ * Batch-insert node rows via multi-value INSERT statements.
186
+ * Each row: [name, kind, file, line, end_line, parent_id, qualified_name, scope, visibility]
187
+ */
188
+ export function batchInsertNodes(db, rows) {
189
+ if (!rows.length) return;
190
+ const ph = '(?,?,?,?,?,?,?,?,?)';
191
+ for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
192
+ const chunk = rows.slice(i, i + BATCH_CHUNK);
193
+ const vals = [];
194
+ for (const r of chunk) vals.push(r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8]);
195
+ db.prepare(
196
+ 'INSERT OR IGNORE INTO nodes (name,kind,file,line,end_line,parent_id,qualified_name,scope,visibility) VALUES ' +
197
+ chunk.map(() => ph).join(','),
198
+ ).run(...vals);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Batch-insert edge rows via multi-value INSERT statements.
204
+ * Each row: [source_id, target_id, kind, confidence, dynamic]
205
+ */
206
+ export function batchInsertEdges(db, rows) {
207
+ if (!rows.length) return;
208
+ const ph = '(?,?,?,?,?)';
209
+ for (let i = 0; i < rows.length; i += BATCH_CHUNK) {
210
+ const chunk = rows.slice(i, i + BATCH_CHUNK);
211
+ const vals = [];
212
+ for (const r of chunk) vals.push(r[0], r[1], r[2], r[3], r[4]);
213
+ db.prepare(
214
+ 'INSERT INTO edges (source_id,target_id,kind,confidence,dynamic) VALUES ' +
215
+ chunk.map(() => ph).join(','),
216
+ ).run(...vals);
217
+ }
218
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Incremental single-file rebuild — used by watch mode.
3
+ *
4
+ * Reuses pipeline helpers instead of duplicating node insertion and edge building
5
+ * logic from the main builder. This eliminates the watcher.js divergence (ROADMAP 3.9).
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { normalizePath } from '../constants.js';
10
+ import { warn } from '../logger.js';
11
+ import { parseFileIncremental } from '../parser.js';
12
+ import { computeConfidence, resolveImportPath } from '../resolve.js';
13
+ import { BUILTIN_RECEIVERS, readFileSafe } from './helpers.js';
14
+
15
+ /**
16
+ * Parse a single file and update the database incrementally.
17
+ *
18
+ * @param {import('better-sqlite3').Database} db
19
+ * @param {string} rootDir - Absolute root directory
20
+ * @param {string} filePath - Absolute file path
21
+ * @param {object} stmts - Prepared DB statements
22
+ * @param {object} engineOpts - Engine options
23
+ * @param {object|null} cache - Parse tree cache (native only)
24
+ * @param {object} [options]
25
+ * @param {Function} [options.diffSymbols] - Symbol diff function
26
+ * @returns {Promise<object|null>} Update result or null on failure
27
+ */
28
+ export async function rebuildFile(_db, rootDir, filePath, stmts, engineOpts, cache, options = {}) {
29
+ const { diffSymbols } = options;
30
+ const relPath = normalizePath(path.relative(rootDir, filePath));
31
+ const oldNodes = stmts.countNodes.get(relPath)?.c || 0;
32
+ const oldSymbols = diffSymbols ? stmts.listSymbols.all(relPath) : [];
33
+
34
+ stmts.deleteEdgesForFile.run(relPath);
35
+ stmts.deleteNodes.run(relPath);
36
+
37
+ if (!fs.existsSync(filePath)) {
38
+ if (cache) cache.remove(filePath);
39
+ const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, []) : null;
40
+ return {
41
+ file: relPath,
42
+ nodesAdded: 0,
43
+ nodesRemoved: oldNodes,
44
+ edgesAdded: 0,
45
+ deleted: true,
46
+ event: 'deleted',
47
+ symbolDiff,
48
+ nodesBefore: oldNodes,
49
+ nodesAfter: 0,
50
+ };
51
+ }
52
+
53
+ let code;
54
+ try {
55
+ code = readFileSafe(filePath);
56
+ } catch (err) {
57
+ warn(`Cannot read ${relPath}: ${err.message}`);
58
+ return null;
59
+ }
60
+
61
+ const symbols = await parseFileIncremental(cache, filePath, code, engineOpts);
62
+ if (!symbols) return null;
63
+
64
+ // Insert nodes
65
+ stmts.insertNode.run(relPath, 'file', relPath, 0, null);
66
+ for (const def of symbols.definitions) {
67
+ stmts.insertNode.run(def.name, def.kind, relPath, def.line, def.endLine || null);
68
+ }
69
+ for (const exp of symbols.exports) {
70
+ stmts.insertNode.run(exp.name, exp.kind, relPath, exp.line, null);
71
+ }
72
+
73
+ const newNodes = stmts.countNodes.get(relPath)?.c || 0;
74
+ const newSymbols = diffSymbols ? stmts.listSymbols.all(relPath) : [];
75
+
76
+ let edgesAdded = 0;
77
+ const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
78
+ if (!fileNodeRow)
79
+ return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
80
+ const fileNodeId = fileNodeRow.id;
81
+
82
+ // Load aliases for import resolution
83
+ const aliases = { baseUrl: null, paths: {} };
84
+
85
+ // Import edges
86
+ for (const imp of symbols.imports) {
87
+ const resolvedPath = resolveImportPath(
88
+ path.join(rootDir, relPath),
89
+ imp.source,
90
+ rootDir,
91
+ aliases,
92
+ );
93
+ const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
94
+ if (targetRow) {
95
+ const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
96
+ stmts.insertEdge.run(fileNodeId, targetRow.id, edgeKind, 1.0, 0);
97
+ edgesAdded++;
98
+ }
99
+ }
100
+
101
+ // Build import name → resolved file mapping
102
+ const importedNames = new Map();
103
+ for (const imp of symbols.imports) {
104
+ const resolvedPath = resolveImportPath(
105
+ path.join(rootDir, relPath),
106
+ imp.source,
107
+ rootDir,
108
+ aliases,
109
+ );
110
+ for (const name of imp.names) {
111
+ importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
112
+ }
113
+ }
114
+
115
+ // Call edges
116
+ for (const call of symbols.calls) {
117
+ if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
118
+
119
+ let caller = null;
120
+ let callerSpan = Infinity;
121
+ for (const def of symbols.definitions) {
122
+ if (def.line <= call.line) {
123
+ const end = def.endLine || Infinity;
124
+ if (call.line <= end) {
125
+ const span = end - def.line;
126
+ if (span < callerSpan) {
127
+ const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
128
+ if (row) {
129
+ caller = row;
130
+ callerSpan = span;
131
+ }
132
+ }
133
+ } else if (!caller) {
134
+ const row = stmts.getNodeId.get(def.name, def.kind, relPath, def.line);
135
+ if (row) caller = row;
136
+ }
137
+ }
138
+ }
139
+ if (!caller) caller = fileNodeRow;
140
+
141
+ const importedFrom = importedNames.get(call.name);
142
+ let targets;
143
+ if (importedFrom) {
144
+ targets = stmts.findNodeInFile.all(call.name, importedFrom);
145
+ }
146
+ if (!targets || targets.length === 0) {
147
+ targets = stmts.findNodeInFile.all(call.name, relPath);
148
+ if (targets.length === 0) {
149
+ targets = stmts.findNodeByName.all(call.name);
150
+ }
151
+ }
152
+
153
+ for (const t of targets) {
154
+ if (t.id !== caller.id) {
155
+ const confidence = importedFrom
156
+ ? computeConfidence(relPath, t.file, importedFrom)
157
+ : computeConfidence(relPath, t.file, null);
158
+ stmts.insertEdge.run(caller.id, t.id, 'calls', confidence, call.dynamic ? 1 : 0);
159
+ edgesAdded++;
160
+ }
161
+ }
162
+ }
163
+
164
+ const symbolDiff = diffSymbols ? diffSymbols(oldSymbols, newSymbols) : null;
165
+ const event = oldNodes === 0 ? 'added' : 'modified';
166
+
167
+ return {
168
+ file: relPath,
169
+ nodesAdded: newNodes,
170
+ nodesRemoved: oldNodes,
171
+ edgesAdded,
172
+ deleted: false,
173
+ event,
174
+ symbolDiff,
175
+ nodesBefore: oldNodes,
176
+ nodesAfter: newNodes,
177
+ };
178
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Pipeline orchestrator — runs build stages sequentially through a shared PipelineContext.
3
+ *
4
+ * This is the heart of the builder refactor (ROADMAP 3.9): the monolithic buildGraph()
5
+ * is decomposed into independently testable stages that communicate via PipelineContext.
6
+ */
7
+ import path from 'node:path';
8
+ import { performance } from 'node:perf_hooks';
9
+ import { loadConfig } from '../config.js';
10
+ import { closeDb, getBuildMeta, initSchema, MIGRATIONS, openDb } from '../db.js';
11
+ import { info } from '../logger.js';
12
+ import { getActiveEngine } from '../parser.js';
13
+ import { PipelineContext } from './context.js';
14
+ import { loadPathAliases } from './helpers.js';
15
+ import { buildEdges } from './stages/build-edges.js';
16
+ import { buildStructure } from './stages/build-structure.js';
17
+ // Pipeline stages
18
+ import { collectFiles } from './stages/collect-files.js';
19
+ import { detectChanges } from './stages/detect-changes.js';
20
+ import { finalize } from './stages/finalize.js';
21
+ import { insertNodes } from './stages/insert-nodes.js';
22
+ import { parseFiles } from './stages/parse-files.js';
23
+ import { resolveImports } from './stages/resolve-imports.js';
24
+ import { runAnalyses } from './stages/run-analyses.js';
25
+
26
+ /**
27
+ * Build the dependency graph for a codebase.
28
+ *
29
+ * Signature and return value are identical to the original monolithic buildGraph().
30
+ *
31
+ * @param {string} rootDir - Root directory to scan
32
+ * @param {object} [opts] - Build options
33
+ * @returns {Promise<{ phases: object } | undefined>}
34
+ */
35
+ export async function buildGraph(rootDir, opts = {}) {
36
+ const ctx = new PipelineContext();
37
+ ctx.buildStart = performance.now();
38
+ ctx.opts = opts;
39
+
40
+ // ── Setup (creates DB, loads config, selects engine) ──────────────
41
+ ctx.rootDir = path.resolve(rootDir);
42
+ ctx.dbPath = path.join(ctx.rootDir, '.codegraph', 'graph.db');
43
+ ctx.db = openDb(ctx.dbPath);
44
+ try {
45
+ initSchema(ctx.db);
46
+
47
+ ctx.config = loadConfig(ctx.rootDir);
48
+ ctx.incremental =
49
+ opts.incremental !== false && ctx.config.build && ctx.config.build.incremental !== false;
50
+
51
+ ctx.engineOpts = {
52
+ engine: opts.engine || 'auto',
53
+ dataflow: opts.dataflow !== false,
54
+ ast: opts.ast !== false,
55
+ };
56
+ const { name: engineName, version: engineVersion } = getActiveEngine(ctx.engineOpts);
57
+ ctx.engineName = engineName;
58
+ ctx.engineVersion = engineVersion;
59
+ info(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
60
+
61
+ // Engine/schema mismatch detection
62
+ ctx.schemaVersion = MIGRATIONS[MIGRATIONS.length - 1].version;
63
+ ctx.forceFullRebuild = false;
64
+ if (ctx.incremental) {
65
+ const prevEngine = getBuildMeta(ctx.db, 'engine');
66
+ if (prevEngine && prevEngine !== engineName) {
67
+ info(`Engine changed (${prevEngine} → ${engineName}), promoting to full rebuild.`);
68
+ ctx.forceFullRebuild = true;
69
+ }
70
+ const prevSchema = getBuildMeta(ctx.db, 'schema_version');
71
+ if (prevSchema && Number(prevSchema) !== ctx.schemaVersion) {
72
+ info(
73
+ `Schema version changed (${prevSchema} → ${ctx.schemaVersion}), promoting to full rebuild.`,
74
+ );
75
+ ctx.forceFullRebuild = true;
76
+ }
77
+ }
78
+
79
+ // Path aliases
80
+ ctx.aliases = loadPathAliases(ctx.rootDir);
81
+ if (ctx.config.aliases) {
82
+ for (const [key, value] of Object.entries(ctx.config.aliases)) {
83
+ const pattern = key.endsWith('/') ? `${key}*` : key;
84
+ const target = path.resolve(ctx.rootDir, value);
85
+ ctx.aliases.paths[pattern] = [target.endsWith('/') ? `${target}*` : `${target}/*`];
86
+ }
87
+ }
88
+ if (ctx.aliases.baseUrl || Object.keys(ctx.aliases.paths).length > 0) {
89
+ info(
90
+ `Loaded path aliases: baseUrl=${ctx.aliases.baseUrl || 'none'}, ${Object.keys(ctx.aliases.paths).length} path mappings`,
91
+ );
92
+ }
93
+
94
+ ctx.timing.setupMs = performance.now() - ctx.buildStart;
95
+
96
+ // ── Pipeline stages ─────────────────────────────────────────────
97
+ await collectFiles(ctx);
98
+ await detectChanges(ctx);
99
+
100
+ if (ctx.earlyExit) return;
101
+
102
+ await parseFiles(ctx);
103
+ await insertNodes(ctx);
104
+ await resolveImports(ctx);
105
+ await buildEdges(ctx);
106
+ await buildStructure(ctx);
107
+ await runAnalyses(ctx);
108
+ await finalize(ctx);
109
+ } catch (err) {
110
+ if (!ctx.earlyExit) closeDb(ctx.db);
111
+ throw err;
112
+ }
113
+
114
+ return {
115
+ phases: {
116
+ setupMs: +ctx.timing.setupMs.toFixed(1),
117
+ parseMs: +(ctx.timing.parseMs ?? 0).toFixed(1),
118
+ insertMs: +(ctx.timing.insertMs ?? 0).toFixed(1),
119
+ resolveMs: +(ctx.timing.resolveMs ?? 0).toFixed(1),
120
+ edgesMs: +(ctx.timing.edgesMs ?? 0).toFixed(1),
121
+ structureMs: +(ctx.timing.structureMs ?? 0).toFixed(1),
122
+ rolesMs: +(ctx.timing.rolesMs ?? 0).toFixed(1),
123
+ astMs: +(ctx.timing.astMs ?? 0).toFixed(1),
124
+ complexityMs: +(ctx.timing.complexityMs ?? 0).toFixed(1),
125
+ ...(ctx.timing.cfgMs != null && { cfgMs: +ctx.timing.cfgMs.toFixed(1) }),
126
+ ...(ctx.timing.dataflowMs != null && { dataflowMs: +ctx.timing.dataflowMs.toFixed(1) }),
127
+ finalizeMs: +(ctx.timing.finalizeMs ?? 0).toFixed(1),
128
+ },
129
+ };
130
+ }