@saluzi/codegraph 0.1.0 → 0.2.0

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 (246) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +885 -0
  3. package/package.json +42 -10
  4. package/scripts/add-lang/bench.sh +60 -0
  5. package/scripts/add-lang/check-grammar.mjs +75 -0
  6. package/scripts/add-lang/dump-ast.mjs +103 -0
  7. package/scripts/add-lang/verify-extraction.mjs +70 -0
  8. package/scripts/agent-eval/ab-adoption.sh +91 -0
  9. package/scripts/agent-eval/ab-hook.sh +86 -0
  10. package/scripts/agent-eval/ab-impl.sh +78 -0
  11. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -0
  12. package/scripts/agent-eval/ab-sufficiency.sh +78 -0
  13. package/scripts/agent-eval/arms-F.sh +21 -0
  14. package/scripts/agent-eval/arms-matrix.sh +37 -0
  15. package/scripts/agent-eval/audit.sh +68 -0
  16. package/scripts/agent-eval/bench-readme.sh +28 -0
  17. package/scripts/agent-eval/bench-why-repo.sh +22 -0
  18. package/scripts/agent-eval/block-read-hook.sh +19 -0
  19. package/scripts/agent-eval/hook-settings.json +15 -0
  20. package/scripts/agent-eval/itrun.sh +120 -0
  21. package/scripts/agent-eval/offload-eval-3arm.sh +72 -0
  22. package/scripts/agent-eval/offload-eval-cost.mjs +133 -0
  23. package/scripts/agent-eval/offload-eval-effort.mjs +108 -0
  24. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -0
  25. package/scripts/agent-eval/offload-eval-frontload.sh +47 -0
  26. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -0
  27. package/scripts/agent-eval/offload-eval-hook.mjs +84 -0
  28. package/scripts/agent-eval/offload-eval-judge.mjs +103 -0
  29. package/scripts/agent-eval/offload-eval-matrix.sh +20 -0
  30. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -0
  31. package/scripts/agent-eval/offload-eval-refs1.sh +50 -0
  32. package/scripts/agent-eval/offload-eval-setup.sh +24 -0
  33. package/scripts/agent-eval/offload-eval-styles.sh +72 -0
  34. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -0
  35. package/scripts/agent-eval/offload-eval.md +76 -0
  36. package/scripts/agent-eval/parse-arms.mjs +116 -0
  37. package/scripts/agent-eval/parse-bench-readme.mjs +84 -0
  38. package/scripts/agent-eval/parse-run.mjs +45 -0
  39. package/scripts/agent-eval/parse-session.mjs +93 -0
  40. package/scripts/agent-eval/probe-context.mjs +21 -0
  41. package/scripts/agent-eval/probe-explore.mjs +40 -0
  42. package/scripts/agent-eval/probe-node.mjs +20 -0
  43. package/scripts/agent-eval/probe-sweep.mjs +119 -0
  44. package/scripts/agent-eval/probe-trace.mjs +20 -0
  45. package/scripts/agent-eval/redirect-read-hook.sh +38 -0
  46. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
  47. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
  48. package/scripts/agent-eval/run-agent.sh +34 -0
  49. package/scripts/agent-eval/run-all.sh +75 -0
  50. package/scripts/agent-eval/run-arms.sh +56 -0
  51. package/scripts/agent-eval/seq-matrix.mjs +137 -0
  52. package/scripts/build-bundle.sh +123 -0
  53. package/scripts/extract-release-notes.mjs +130 -0
  54. package/scripts/local-install.sh +41 -0
  55. package/scripts/npm-sdk.js +75 -0
  56. package/scripts/npm-shim.js +275 -0
  57. package/scripts/pack-npm.sh +119 -0
  58. package/scripts/prepare-release.mjs +270 -0
  59. package/dist/bin/codegraph.d.ts +0 -25
  60. package/dist/bin/node-version-check.d.ts +0 -37
  61. package/dist/bin/uninstall.d.ts +0 -14
  62. package/dist/context/formatter.d.ts +0 -33
  63. package/dist/context/formatter.js +0 -244
  64. package/dist/context/index.d.ts +0 -117
  65. package/dist/context/index.js +0 -1050
  66. package/dist/db/index.d.ts +0 -101
  67. package/dist/db/index.js +0 -250
  68. package/dist/db/migrations.d.ts +0 -47
  69. package/dist/db/migrations.js +0 -131
  70. package/dist/db/queries.d.ts +0 -291
  71. package/dist/db/queries.js +0 -1349
  72. package/dist/db/schema.sql +0 -151
  73. package/dist/db/sqlite-adapter.d.ts +0 -49
  74. package/dist/db/sqlite-adapter.js +0 -141
  75. package/dist/directory.d.ts +0 -62
  76. package/dist/directory.js +0 -264
  77. package/dist/errors.d.ts +0 -149
  78. package/dist/errors.js +0 -219
  79. package/dist/extraction/dfm-extractor.d.ts +0 -31
  80. package/dist/extraction/dfm-extractor.js +0 -151
  81. package/dist/extraction/grammars.d.ts +0 -94
  82. package/dist/extraction/grammars.js +0 -357
  83. package/dist/extraction/index.d.ts +0 -148
  84. package/dist/extraction/index.js +0 -1286
  85. package/dist/extraction/languages/c-cpp.d.ts +0 -4
  86. package/dist/extraction/languages/c-cpp.js +0 -126
  87. package/dist/extraction/languages/csharp.d.ts +0 -3
  88. package/dist/extraction/languages/csharp.js +0 -72
  89. package/dist/extraction/languages/dart.d.ts +0 -3
  90. package/dist/extraction/languages/dart.js +0 -192
  91. package/dist/extraction/languages/go.d.ts +0 -3
  92. package/dist/extraction/languages/go.js +0 -58
  93. package/dist/extraction/languages/index.d.ts +0 -10
  94. package/dist/extraction/languages/index.js +0 -49
  95. package/dist/extraction/languages/java.d.ts +0 -3
  96. package/dist/extraction/languages/java.js +0 -64
  97. package/dist/extraction/languages/javascript.d.ts +0 -3
  98. package/dist/extraction/languages/javascript.js +0 -90
  99. package/dist/extraction/languages/kotlin.d.ts +0 -3
  100. package/dist/extraction/languages/kotlin.js +0 -253
  101. package/dist/extraction/languages/lua.d.ts +0 -3
  102. package/dist/extraction/languages/lua.js +0 -150
  103. package/dist/extraction/languages/luau.d.ts +0 -3
  104. package/dist/extraction/languages/luau.js +0 -37
  105. package/dist/extraction/languages/pascal.d.ts +0 -3
  106. package/dist/extraction/languages/pascal.js +0 -66
  107. package/dist/extraction/languages/php.d.ts +0 -3
  108. package/dist/extraction/languages/php.js +0 -107
  109. package/dist/extraction/languages/python.d.ts +0 -3
  110. package/dist/extraction/languages/python.js +0 -56
  111. package/dist/extraction/languages/ruby.d.ts +0 -3
  112. package/dist/extraction/languages/ruby.js +0 -114
  113. package/dist/extraction/languages/rust.d.ts +0 -3
  114. package/dist/extraction/languages/rust.js +0 -109
  115. package/dist/extraction/languages/scala.d.ts +0 -3
  116. package/dist/extraction/languages/scala.js +0 -139
  117. package/dist/extraction/languages/swift.d.ts +0 -3
  118. package/dist/extraction/languages/swift.js +0 -91
  119. package/dist/extraction/languages/typescript.d.ts +0 -3
  120. package/dist/extraction/languages/typescript.js +0 -129
  121. package/dist/extraction/liquid-extractor.d.ts +0 -52
  122. package/dist/extraction/liquid-extractor.js +0 -313
  123. package/dist/extraction/parse-worker.d.ts +0 -8
  124. package/dist/extraction/parse-worker.js +0 -94
  125. package/dist/extraction/svelte-extractor.d.ts +0 -56
  126. package/dist/extraction/svelte-extractor.js +0 -272
  127. package/dist/extraction/tree-sitter-helpers.d.ts +0 -39
  128. package/dist/extraction/tree-sitter-helpers.js +0 -103
  129. package/dist/extraction/tree-sitter-types.d.ts +0 -191
  130. package/dist/extraction/tree-sitter-types.js +0 -10
  131. package/dist/extraction/tree-sitter.d.ts +0 -238
  132. package/dist/extraction/tree-sitter.js +0 -2430
  133. package/dist/extraction/vue-extractor.d.ts +0 -36
  134. package/dist/extraction/vue-extractor.js +0 -163
  135. package/dist/extraction/wasm/tree-sitter-c.wasm +0 -0
  136. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  137. package/dist/extraction/wasm/tree-sitter-cpp.wasm +0 -0
  138. package/dist/extraction/wasm/tree-sitter-dart.wasm +0 -0
  139. package/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
  140. package/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
  141. package/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
  142. package/dist/extraction/wasm/tree-sitter-kotlin.wasm +0 -0
  143. package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
  144. package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
  145. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  146. package/dist/extraction/wasm/tree-sitter-php.wasm +0 -0
  147. package/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
  148. package/dist/extraction/wasm/tree-sitter-ruby.wasm +0 -0
  149. package/dist/extraction/wasm/tree-sitter-rust.wasm +0 -0
  150. package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  151. package/dist/extraction/wasm/tree-sitter-swift.wasm +0 -0
  152. package/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
  153. package/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
  154. package/dist/extraction/wasm-runtime-flags.d.ts +0 -46
  155. package/dist/extraction/wasm-runtime-flags.js +0 -105
  156. package/dist/graph/index.d.ts +0 -8
  157. package/dist/graph/index.js +0 -13
  158. package/dist/graph/queries.d.ts +0 -109
  159. package/dist/graph/queries.js +0 -366
  160. package/dist/graph/traversal.d.ts +0 -137
  161. package/dist/graph/traversal.js +0 -528
  162. package/dist/index.d.ts +0 -509
  163. package/dist/index.js +0 -800
  164. package/dist/installer/claude-md-template.d.ts +0 -19
  165. package/dist/installer/config-writer.d.ts +0 -29
  166. package/dist/installer/index.d.ts +0 -140
  167. package/dist/installer/instructions-template.d.ts +0 -30
  168. package/dist/installer/targets/claude.d.ts +0 -55
  169. package/dist/installer/targets/codex.d.ts +0 -18
  170. package/dist/installer/targets/cursor.d.ts +0 -35
  171. package/dist/installer/targets/hermes.d.ts +0 -18
  172. package/dist/installer/targets/opencode.d.ts +0 -30
  173. package/dist/installer/targets/registry.d.ts +0 -38
  174. package/dist/installer/targets/shared.d.ts +0 -92
  175. package/dist/installer/targets/toml.d.ts +0 -64
  176. package/dist/installer/targets/types.d.ts +0 -122
  177. package/dist/mcp/index.d.ts +0 -98
  178. package/dist/mcp/server-instructions.d.ts +0 -20
  179. package/dist/mcp/tools.d.ts +0 -269
  180. package/dist/mcp/transport.d.ts +0 -117
  181. package/dist/resolution/frameworks/cargo-workspace.d.ts +0 -20
  182. package/dist/resolution/frameworks/cargo-workspace.js +0 -225
  183. package/dist/resolution/frameworks/csharp.d.ts +0 -8
  184. package/dist/resolution/frameworks/csharp.js +0 -213
  185. package/dist/resolution/frameworks/drupal.d.ts +0 -51
  186. package/dist/resolution/frameworks/drupal.js +0 -335
  187. package/dist/resolution/frameworks/express.d.ts +0 -8
  188. package/dist/resolution/frameworks/express.js +0 -225
  189. package/dist/resolution/frameworks/go.d.ts +0 -8
  190. package/dist/resolution/frameworks/go.js +0 -158
  191. package/dist/resolution/frameworks/index.d.ts +0 -52
  192. package/dist/resolution/frameworks/index.js +0 -137
  193. package/dist/resolution/frameworks/java.d.ts +0 -8
  194. package/dist/resolution/frameworks/java.js +0 -177
  195. package/dist/resolution/frameworks/laravel.d.ts +0 -13
  196. package/dist/resolution/frameworks/laravel.js +0 -248
  197. package/dist/resolution/frameworks/nestjs.d.ts +0 -26
  198. package/dist/resolution/frameworks/nestjs.js +0 -374
  199. package/dist/resolution/frameworks/python.d.ts +0 -10
  200. package/dist/resolution/frameworks/python.js +0 -278
  201. package/dist/resolution/frameworks/react.d.ts +0 -8
  202. package/dist/resolution/frameworks/react.js +0 -272
  203. package/dist/resolution/frameworks/ruby.d.ts +0 -8
  204. package/dist/resolution/frameworks/ruby.js +0 -198
  205. package/dist/resolution/frameworks/rust.d.ts +0 -8
  206. package/dist/resolution/frameworks/rust.js +0 -207
  207. package/dist/resolution/frameworks/svelte.d.ts +0 -9
  208. package/dist/resolution/frameworks/svelte.js +0 -249
  209. package/dist/resolution/frameworks/swift.d.ts +0 -10
  210. package/dist/resolution/frameworks/swift.js +0 -376
  211. package/dist/resolution/frameworks/vue.d.ts +0 -9
  212. package/dist/resolution/frameworks/vue.js +0 -306
  213. package/dist/resolution/import-resolver.d.ts +0 -61
  214. package/dist/resolution/import-resolver.js +0 -663
  215. package/dist/resolution/index.d.ts +0 -118
  216. package/dist/resolution/index.js +0 -744
  217. package/dist/resolution/lru-cache.d.ts +0 -24
  218. package/dist/resolution/lru-cache.js +0 -62
  219. package/dist/resolution/name-matcher.d.ts +0 -50
  220. package/dist/resolution/name-matcher.js +0 -384
  221. package/dist/resolution/path-aliases.d.ts +0 -72
  222. package/dist/resolution/path-aliases.js +0 -238
  223. package/dist/resolution/strip-comments.d.ts +0 -40
  224. package/dist/resolution/strip-comments.js +0 -441
  225. package/dist/resolution/types.d.ts +0 -181
  226. package/dist/resolution/types.js +0 -8
  227. package/dist/search/query-parser.d.ts +0 -61
  228. package/dist/search/query-parser.js +0 -177
  229. package/dist/search/query-utils.d.ts +0 -59
  230. package/dist/search/query-utils.js +0 -383
  231. package/dist/sync/git-hooks.d.ts +0 -54
  232. package/dist/sync/git-hooks.js +0 -223
  233. package/dist/sync/index.d.ts +0 -25
  234. package/dist/sync/index.js +0 -28
  235. package/dist/sync/watch-policy.d.ts +0 -51
  236. package/dist/sync/watch-policy.js +0 -124
  237. package/dist/sync/watcher.d.ts +0 -83
  238. package/dist/sync/watcher.js +0 -192
  239. package/dist/types.d.ts +0 -433
  240. package/dist/types.js +0 -75
  241. package/dist/ui/glyphs.d.ts +0 -42
  242. package/dist/ui/shimmer-progress.d.ts +0 -11
  243. package/dist/ui/shimmer-worker.d.ts +0 -2
  244. package/dist/ui/types.d.ts +0 -20
  245. package/dist/utils.d.ts +0 -231
  246. package/dist/utils.js +0 -549
@@ -1,1286 +0,0 @@
1
- "use strict";
2
- /**
3
- * Extraction Orchestrator
4
- *
5
- * Coordinates file scanning, parsing, and database storage.
6
- */
7
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
- if (k2 === undefined) k2 = k;
9
- var desc = Object.getOwnPropertyDescriptor(m, k);
10
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
- desc = { enumerable: true, get: function() { return m[k]; } };
12
- }
13
- Object.defineProperty(o, k2, desc);
14
- }) : (function(o, m, k, k2) {
15
- if (k2 === undefined) k2 = k;
16
- o[k2] = m[k];
17
- }));
18
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
- Object.defineProperty(o, "default", { enumerable: true, value: v });
20
- }) : function(o, v) {
21
- o["default"] = v;
22
- });
23
- var __importStar = (this && this.__importStar) || (function () {
24
- var ownKeys = function(o) {
25
- ownKeys = Object.getOwnPropertyNames || function (o) {
26
- var ar = [];
27
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
- return ar;
29
- };
30
- return ownKeys(o);
31
- };
32
- return function (mod) {
33
- if (mod && mod.__esModule) return mod;
34
- var result = {};
35
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
- __setModuleDefault(result, mod);
37
- return result;
38
- };
39
- })();
40
- var __importDefault = (this && this.__importDefault) || function (mod) {
41
- return (mod && mod.__esModule) ? mod : { "default": mod };
42
- };
43
- Object.defineProperty(exports, "__esModule", { value: true });
44
- exports.loadAllGrammars = exports.loadGrammarsForLanguages = exports.initGrammars = exports.getSupportedLanguages = exports.isGrammarLoaded = exports.isLanguageSupported = exports.isSourceFile = exports.detectLanguage = exports.extractFromSource = exports.ExtractionOrchestrator = void 0;
45
- exports.hashContent = hashContent;
46
- exports.scanDirectory = scanDirectory;
47
- exports.scanDirectoryAsync = scanDirectoryAsync;
48
- const fs = __importStar(require("fs"));
49
- const fsp = __importStar(require("fs/promises"));
50
- const path = __importStar(require("path"));
51
- const crypto = __importStar(require("crypto"));
52
- const child_process_1 = require("child_process");
53
- const tree_sitter_1 = require("./tree-sitter");
54
- const grammars_1 = require("./grammars");
55
- const errors_1 = require("../errors");
56
- const utils_1 = require("../utils");
57
- const ignore_1 = __importDefault(require("ignore"));
58
- const frameworks_1 = require("../resolution/frameworks");
59
- /**
60
- * Number of files to read in parallel during indexing.
61
- * File reads are I/O-bound; batching overlaps I/O wait with CPU parse work.
62
- */
63
- const FILE_IO_BATCH_SIZE = 10;
64
- // PARSER_RESET_INTERVAL moved to parse-worker.ts (runs in worker thread)
65
- /**
66
- * Maximum time (ms) to wait for a single file to parse in the worker thread.
67
- * If tree-sitter hangs or WASM runs out of memory, this prevents the entire
68
- * indexing run from freezing. The worker is restarted after a timeout.
69
- */
70
- const PARSE_TIMEOUT_MS = 10_000;
71
- /**
72
- * Number of files to parse before recycling the worker thread.
73
- * WASM linear memory can grow but NEVER shrink (WebAssembly spec limitation).
74
- * The only way to reclaim tree-sitter's WASM heap is to destroy the entire
75
- * V8 isolate by terminating the worker thread and spawning a fresh one.
76
- * This interval balances memory usage against the cost of reloading grammars.
77
- */
78
- const WORKER_RECYCLE_INTERVAL = 250;
79
- /**
80
- * Calculate SHA256 hash of file contents
81
- */
82
- function hashContent(content) {
83
- return crypto.createHash('sha256').update(content).digest('hex');
84
- }
85
- /**
86
- * Skip files larger than this (bytes). Generated bundles, minified JS, and
87
- * vendored blobs blow the WASM heap and the worker-recycle budget for no useful
88
- * symbols. 1 MB covers essentially all hand-written source.
89
- */
90
- const MAX_FILE_SIZE = 1024 * 1024;
91
- /**
92
- * Collect git-visible files (tracked + untracked, .gitignore-respected) from the
93
- * git repository rooted at `repoDir`, adding each to `files` with `prefix`
94
- * prepended so paths stay relative to the original scan root.
95
- *
96
- * Recurses into embedded git repositories — nested repos that are NOT submodules
97
- * (independent clones living inside the workspace, common in CMake "super-repo"
98
- * layouts). The parent repo's `git ls-files` cannot see into them: tracked output
99
- * skips them entirely, and untracked output reports them only as an opaque
100
- * "subdir/" entry (trailing slash) rather than expanding their files. Each
101
- * embedded repo is its own git boundary, so we re-run `git ls-files` inside it.
102
- * (See issue #193.)
103
- */
104
- function collectGitFiles(repoDir, prefix, files) {
105
- const gitOpts = { cwd: repoDir, encoding: 'utf-8', timeout: 30000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] };
106
- // Tracked files. --recurse-submodules pulls in files from active submodules,
107
- // which the index would otherwise represent only as a commit pointer.
108
- // Without this, monorepos using submodules index 0 files. (See issue #147.)
109
- // Note: --recurse-submodules only supports -c/--cached and --stage modes — it
110
- // can't be combined with -o, so untracked files are gathered separately below.
111
- const tracked = (0, child_process_1.execFileSync)('git', ['ls-files', '-c', '--recurse-submodules'], gitOpts);
112
- for (const line of tracked.split('\n')) {
113
- const trimmed = line.trim();
114
- if (trimmed) {
115
- files.add((0, utils_1.normalizePath)(prefix + trimmed));
116
- }
117
- }
118
- // Untracked files (submodules manage their own untracked state). Embedded git
119
- // repos surface here as a single "subdir/" entry that git refuses to descend
120
- // into — recurse into those as their own repos so their source gets indexed.
121
- const untracked = (0, child_process_1.execFileSync)('git', ['ls-files', '-o', '--exclude-standard'], gitOpts);
122
- for (const line of untracked.split('\n')) {
123
- const trimmed = line.trim();
124
- if (!trimmed)
125
- continue;
126
- if (trimmed.endsWith('/')) {
127
- // git only emits a trailing-slash directory entry for an embedded repo.
128
- // Guard with a .git check anyway, and skip anything else exactly as git
129
- // itself skips it (we never descend into a non-repo opaque dir).
130
- const childDir = path.join(repoDir, trimmed);
131
- if (fs.existsSync(path.join(childDir, '.git'))) {
132
- collectGitFiles(childDir, prefix + trimmed, files);
133
- }
134
- continue;
135
- }
136
- files.add((0, utils_1.normalizePath)(prefix + trimmed));
137
- }
138
- }
139
- /**
140
- * Get all files visible to git (tracked + untracked but not ignored).
141
- * Respects .gitignore at all levels (root, subdirectories) and descends into
142
- * embedded (nested, non-submodule) git repos. Returns null on failure
143
- * (non-git project) so callers can fall back to a filesystem walk.
144
- */
145
- function getGitVisibleFiles(rootDir) {
146
- try {
147
- // Check if the project directory is gitignored by a parent repo.
148
- // When rootDir lives inside a parent git repo that ignores it,
149
- // `git ls-files` returns nothing — fall back to filesystem walk.
150
- const gitRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
151
- if (path.resolve(gitRoot) !== path.resolve(rootDir)) {
152
- try {
153
- // git check-ignore exits 0 if the path IS ignored, 1 if not
154
- (0, child_process_1.execFileSync)('git', ['check-ignore', '-q', path.resolve(rootDir)], { cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
155
- // Directory is gitignored by parent repo — fall back to filesystem walk
156
- return null;
157
- }
158
- catch {
159
- // Not ignored — safe to use git ls-files
160
- }
161
- }
162
- const files = new Set();
163
- collectGitFiles(rootDir, '', files);
164
- return files;
165
- }
166
- catch {
167
- return null;
168
- }
169
- }
170
- /**
171
- * Use `git status` to detect changed files instead of scanning every file.
172
- * Returns null on failure so callers fall back to full scan.
173
- */
174
- function getGitChangedFiles(rootDir) {
175
- try {
176
- const output = (0, child_process_1.execFileSync)('git', ['status', '--porcelain', '--no-renames'], { cwd: rootDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
177
- const modified = [];
178
- const added = [];
179
- const deleted = [];
180
- for (const line of output.split('\n')) {
181
- if (line.length < 4)
182
- continue; // Minimum: "XY file"
183
- const statusCode = line.substring(0, 2);
184
- const filePath = (0, utils_1.normalizePath)(line.substring(3));
185
- // Skip non-source files (git status already omits .gitignored paths).
186
- if (!(0, grammars_1.isSourceFile)(filePath))
187
- continue;
188
- if (statusCode === '??') {
189
- added.push(filePath);
190
- }
191
- else if (statusCode.includes('D')) {
192
- deleted.push(filePath);
193
- }
194
- else {
195
- // M, MM, AM, A (staged), etc. — treat as modified
196
- modified.push(filePath);
197
- }
198
- }
199
- return { modified, added, deleted };
200
- }
201
- catch {
202
- return null;
203
- }
204
- }
205
- /**
206
- * Recursively scan a directory for source files.
207
- *
208
- * In git repos, uses `git ls-files` (inherently respects .gitignore at all
209
- * levels), then keeps files with a supported source extension. For non-git
210
- * projects, falls back to a filesystem walk that parses .gitignore itself.
211
- */
212
- function scanDirectory(rootDir, onProgress) {
213
- // Fast path: use git to get all visible files (respects .gitignore everywhere)
214
- const gitFiles = getGitVisibleFiles(rootDir);
215
- if (gitFiles) {
216
- const files = [];
217
- let count = 0;
218
- for (const filePath of gitFiles) {
219
- if ((0, grammars_1.isSourceFile)(filePath)) {
220
- files.push(filePath);
221
- count++;
222
- onProgress?.(count, filePath);
223
- }
224
- }
225
- return files;
226
- }
227
- // Fallback: walk filesystem for non-git projects
228
- return scanDirectoryWalk(rootDir, onProgress);
229
- }
230
- /**
231
- * Async variant of scanDirectory that yields to the event loop periodically,
232
- * allowing worker threads to receive and render progress messages.
233
- */
234
- async function scanDirectoryAsync(rootDir, onProgress) {
235
- const gitFiles = getGitVisibleFiles(rootDir);
236
- if (gitFiles) {
237
- const files = [];
238
- let count = 0;
239
- for (const filePath of gitFiles) {
240
- if ((0, grammars_1.isSourceFile)(filePath)) {
241
- files.push(filePath);
242
- count++;
243
- onProgress?.(count, filePath);
244
- // Yield every 100 files so worker threads can render progress
245
- if (count % 100 === 0) {
246
- await new Promise(r => setImmediate(r));
247
- }
248
- }
249
- }
250
- return files;
251
- }
252
- return scanDirectoryWalk(rootDir, onProgress);
253
- }
254
- /**
255
- * Filesystem walk fallback for non-git projects.
256
- */
257
- function scanDirectoryWalk(rootDir, onProgress) {
258
- const files = [];
259
- let count = 0;
260
- const visitedDirs = new Set();
261
- const loadIgnore = (dir) => {
262
- try {
263
- const giPath = path.join(dir, '.gitignore');
264
- if (fs.existsSync(giPath)) {
265
- return { dir, ig: (0, ignore_1.default)().add(fs.readFileSync(giPath, 'utf-8')) };
266
- }
267
- }
268
- catch {
269
- // Unreadable .gitignore — treat as absent.
270
- }
271
- return null;
272
- };
273
- const isIgnored = (fullPath, isDir, matchers) => {
274
- for (const { dir, ig } of matchers) {
275
- let rel = (0, utils_1.normalizePath)(path.relative(dir, fullPath));
276
- if (!rel || rel.startsWith('..'))
277
- continue; // not under this matcher's dir
278
- if (isDir)
279
- rel += '/'; // dir-only rules (e.g. `build/`) only match with the slash
280
- if (ig.ignores(rel))
281
- return true;
282
- }
283
- return false;
284
- };
285
- function walk(dir, matchers) {
286
- let realDir;
287
- try {
288
- realDir = fs.realpathSync(dir);
289
- }
290
- catch {
291
- (0, errors_1.logDebug)('Skipping unresolvable directory', { dir });
292
- return;
293
- }
294
- if (visitedDirs.has(realDir)) {
295
- (0, errors_1.logDebug)('Skipping already-visited directory (symlink cycle)', { dir, realDir });
296
- return;
297
- }
298
- visitedDirs.add(realDir);
299
- // This directory's own .gitignore (if present) applies to everything below it.
300
- const own = loadIgnore(dir);
301
- const active = own ? [...matchers, own] : matchers;
302
- let entries;
303
- try {
304
- entries = fs.readdirSync(dir, { withFileTypes: true });
305
- }
306
- catch (error) {
307
- (0, errors_1.logDebug)('Skipping unreadable directory', { dir, error: String(error) });
308
- return;
309
- }
310
- for (const entry of entries) {
311
- // Never descend into git internals or our own data directory.
312
- if (entry.name === '.git' || entry.name === '.codegraph')
313
- continue;
314
- const fullPath = path.join(dir, entry.name);
315
- const relativePath = (0, utils_1.normalizePath)(path.relative(rootDir, fullPath));
316
- if (entry.isSymbolicLink()) {
317
- try {
318
- const realTarget = fs.realpathSync(fullPath);
319
- const stat = fs.statSync(realTarget);
320
- if (stat.isDirectory()) {
321
- if (!isIgnored(fullPath, true, active)) {
322
- walk(fullPath, active);
323
- }
324
- }
325
- else if (stat.isFile()) {
326
- if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath)) {
327
- files.push(relativePath);
328
- count++;
329
- onProgress?.(count, relativePath);
330
- }
331
- }
332
- }
333
- catch {
334
- (0, errors_1.logDebug)('Skipping broken symlink', { path: fullPath });
335
- }
336
- continue;
337
- }
338
- if (entry.isDirectory()) {
339
- if (!isIgnored(fullPath, true, active)) {
340
- walk(fullPath, active);
341
- }
342
- }
343
- else if (entry.isFile()) {
344
- if (!isIgnored(fullPath, false, active) && (0, grammars_1.isSourceFile)(relativePath)) {
345
- files.push(relativePath);
346
- count++;
347
- onProgress?.(count, relativePath);
348
- }
349
- }
350
- }
351
- }
352
- walk(rootDir, []);
353
- return files;
354
- }
355
- /**
356
- * Extraction orchestrator
357
- */
358
- class ExtractionOrchestrator {
359
- rootDir;
360
- queries;
361
- /**
362
- * Names of frameworks detected for this project, populated by indexAll().
363
- * Passed to extractFromSource so framework-specific extractors (route nodes,
364
- * middleware, etc.) run after the tree-sitter pass. Cleared if detection
365
- * hasn't run yet so single-file re-index paths can detect on the spot.
366
- */
367
- detectedFrameworkNames = null;
368
- constructor(rootDir, queries) {
369
- this.rootDir = rootDir;
370
- this.queries = queries;
371
- }
372
- /**
373
- * Build a filesystem-backed ResolutionContext sufficient for framework
374
- * detection. Graph-query methods (getNodesByName etc.) return empty because
375
- * the DB hasn't been populated yet, but detect() only uses readFile,
376
- * fileExists, and getAllFiles, so that's fine.
377
- */
378
- buildDetectionContext(files) {
379
- const rootDir = this.rootDir;
380
- return {
381
- getNodesInFile: () => [],
382
- getNodesByName: () => [],
383
- getNodesByQualifiedName: () => [],
384
- getNodesByKind: () => [],
385
- getNodesByLowerName: () => [],
386
- getImportMappings: () => [],
387
- getAllFiles: () => files,
388
- getProjectRoot: () => rootDir,
389
- fileExists: (relativePath) => {
390
- const full = (0, utils_1.validatePathWithinRoot)(rootDir, relativePath);
391
- if (!full)
392
- return false;
393
- try {
394
- return fs.existsSync(full);
395
- }
396
- catch {
397
- return false;
398
- }
399
- },
400
- readFile: (relativePath) => {
401
- const full = (0, utils_1.validatePathWithinRoot)(rootDir, relativePath);
402
- if (!full)
403
- return null;
404
- try {
405
- return fs.readFileSync(full, 'utf-8');
406
- }
407
- catch {
408
- return null;
409
- }
410
- },
411
- };
412
- }
413
- /**
414
- * Detect frameworks on demand using the current scanned files (or a fresh
415
- * scan if none are provided). Cached on the orchestrator so repeat calls
416
- * inside a single run don't re-scan.
417
- */
418
- ensureDetectedFrameworks(files) {
419
- if (this.detectedFrameworkNames !== null)
420
- return this.detectedFrameworkNames;
421
- const fileList = files ?? scanDirectory(this.rootDir);
422
- const context = this.buildDetectionContext(fileList);
423
- this.detectedFrameworkNames = (0, frameworks_1.detectFrameworks)(context).map((r) => r.name);
424
- return this.detectedFrameworkNames;
425
- }
426
- /**
427
- * Index all files in the project
428
- */
429
- async indexAll(onProgress, signal, verbose) {
430
- await (0, grammars_1.initGrammars)();
431
- const startTime = Date.now();
432
- const errors = [];
433
- let filesIndexed = 0;
434
- let filesSkipped = 0;
435
- let filesErrored = 0;
436
- let totalNodes = 0;
437
- let totalEdges = 0;
438
- const log = verbose
439
- ? (msg) => { console.log(`[worker] ${msg}`); }
440
- : (_msg) => { };
441
- // Phase 1: Scan for files
442
- onProgress?.({
443
- phase: 'scanning',
444
- current: 0,
445
- total: 0,
446
- });
447
- const files = await scanDirectoryAsync(this.rootDir, (current, file) => {
448
- onProgress?.({
449
- phase: 'scanning',
450
- current,
451
- total: 0,
452
- currentFile: file,
453
- });
454
- });
455
- // Detect frameworks once per indexAll run using the scanned file list.
456
- // Names are passed to each parse call so framework-specific extractors
457
- // (route nodes, middleware, etc.) run after the tree-sitter pass.
458
- // Framework detection is reset each run so adding e.g. requirements.txt
459
- // between runs is picked up without restarting the process.
460
- this.detectedFrameworkNames = null;
461
- const frameworkNames = this.ensureDetectedFrameworks(files);
462
- if (signal?.aborted) {
463
- return {
464
- success: false,
465
- filesIndexed: 0,
466
- filesSkipped: 0,
467
- filesErrored: 0,
468
- nodesCreated: 0,
469
- edgesCreated: 0,
470
- errors: [{ message: 'Aborted', severity: 'error' }],
471
- durationMs: Date.now() - startTime,
472
- };
473
- }
474
- // Phase 2: Parse files in a worker thread (keeps main thread unblocked for UI)
475
- const total = files.length;
476
- let processed = 0;
477
- // Emit parsing phase immediately so the progress bar appears during worker setup.
478
- // The yield lets the shimmer worker flush the phase transition to stdout before
479
- // the main thread starts synchronous grammar detection work.
480
- onProgress?.({
481
- phase: 'parsing',
482
- current: 0,
483
- total,
484
- });
485
- await new Promise(resolve => setImmediate(resolve));
486
- // Detect needed languages and load grammars in the parse worker
487
- const neededLanguages = [...new Set(files.map((f) => (0, grammars_1.detectLanguage)(f)))];
488
- // .h files default to 'c' but may be C++ — ensure cpp grammar is loaded when c is needed
489
- if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
490
- neededLanguages.push('cpp');
491
- }
492
- // Try to use a worker thread for parsing (keeps main thread unblocked for UI).
493
- // Falls back to in-process parsing if the compiled worker is unavailable (e.g. tests).
494
- const parseWorkerPath = path.join(__dirname, 'parse-worker.js');
495
- const useWorker = fs.existsSync(parseWorkerPath);
496
- let WorkerClass = null;
497
- if (useWorker) {
498
- const { Worker } = await Promise.resolve().then(() => __importStar(require('worker_threads')));
499
- WorkerClass = Worker;
500
- }
501
- else {
502
- // In-process fallback: load grammars locally
503
- await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
504
- }
505
- // --- Worker lifecycle management ---
506
- // The worker can crash (OOM in WASM) or hang on pathological files.
507
- // We track pending parse promises and handle both cases:
508
- // - Timeout: terminate + restart the worker, reject the timed-out request
509
- // - Crash: reject all pending promises, restart for remaining files
510
- let parseWorker = null;
511
- let nextId = 0;
512
- let workerParseCount = 0;
513
- const pendingParses = new Map();
514
- function rejectAllPending(reason) {
515
- for (const [id, pending] of pendingParses) {
516
- clearTimeout(pending.timer);
517
- pendingParses.delete(id);
518
- pending.reject(new Error(reason));
519
- }
520
- }
521
- function attachWorkerHandlers(w) {
522
- w.on('message', (msg) => {
523
- if (msg.type === 'parse-result' && msg.id !== undefined) {
524
- const pending = pendingParses.get(msg.id);
525
- if (pending) {
526
- clearTimeout(pending.timer);
527
- pendingParses.delete(msg.id);
528
- pending.resolve(msg.result);
529
- }
530
- }
531
- });
532
- w.on('error', (err) => {
533
- (0, errors_1.logWarn)('Parse worker error', { error: err.message });
534
- rejectAllPending(`Worker error: ${err.message}`);
535
- });
536
- w.on('exit', (code) => {
537
- if (code !== 0 && pendingParses.size > 0) {
538
- (0, errors_1.logWarn)('Parse worker exited unexpectedly', { code });
539
- rejectAllPending(`Worker exited with code ${code}`);
540
- }
541
- // Clear reference so we know to respawn, reset count so
542
- // the fresh worker gets a full cycle before recycling.
543
- if (parseWorker === w) {
544
- parseWorker = null;
545
- workerParseCount = 0;
546
- }
547
- });
548
- }
549
- async function ensureWorker() {
550
- if (parseWorker)
551
- return parseWorker;
552
- log('Spawning new parse worker...');
553
- parseWorker = new WorkerClass(parseWorkerPath);
554
- attachWorkerHandlers(parseWorker);
555
- // Load grammars in the new worker
556
- await new Promise((resolve, reject) => {
557
- parseWorker.once('message', (msg) => {
558
- if (msg.type === 'grammars-loaded')
559
- resolve();
560
- else
561
- reject(new Error(`Unexpected message: ${msg.type}`));
562
- });
563
- parseWorker.postMessage({ type: 'load-grammars', languages: neededLanguages });
564
- });
565
- return parseWorker;
566
- }
567
- if (WorkerClass) {
568
- await ensureWorker();
569
- }
570
- /**
571
- * Recycle the worker thread to reclaim WASM memory.
572
- * Terminates the current worker and clears the reference so
573
- * ensureWorker() will spawn a fresh one on the next call.
574
- */
575
- function recycleWorker() {
576
- if (!parseWorker)
577
- return;
578
- log(`Recycling worker after ${workerParseCount} parses (heap: ${Math.round(process.memoryUsage().rss / 1024 / 1024)}MB RSS)`);
579
- const w = parseWorker;
580
- parseWorker = null;
581
- workerParseCount = 0;
582
- // Fire-and-forget: worker.terminate() can hang if WASM is stuck
583
- w.terminate().catch(() => { });
584
- }
585
- async function requestParse(filePath, content) {
586
- if (!WorkerClass) {
587
- // In-process fallback
588
- return (0, tree_sitter_1.extractFromSource)(filePath, content, (0, grammars_1.detectLanguage)(filePath, content), frameworkNames);
589
- }
590
- // Recycle the worker before the next parse if we've hit the threshold.
591
- // This destroys the WASM linear memory (which can grow but never shrink)
592
- // and starts a fresh worker with a clean heap.
593
- if (workerParseCount >= WORKER_RECYCLE_INTERVAL) {
594
- await recycleWorker();
595
- }
596
- const worker = await ensureWorker();
597
- const id = nextId++;
598
- workerParseCount++;
599
- // Scale timeout for large files: base 10s + 10s per 100KB
600
- const timeoutMs = PARSE_TIMEOUT_MS + Math.floor(content.length / 100_000) * 10_000;
601
- return new Promise((resolve, reject) => {
602
- const timer = setTimeout(() => {
603
- pendingParses.delete(id);
604
- log(`TIMEOUT: ${filePath} exceeded ${timeoutMs}ms — killing worker`);
605
- // Reject FIRST — worker.terminate() can hang if WASM is stuck
606
- parseWorker = null;
607
- workerParseCount = 0;
608
- reject(new Error(`Parse timed out after ${timeoutMs}ms`));
609
- // Fire-and-forget: kill the stuck worker in the background
610
- worker.terminate().catch(() => { });
611
- }, timeoutMs);
612
- pendingParses.set(id, { resolve, reject, timer });
613
- worker.postMessage({ type: 'parse', id, filePath, content, frameworkNames });
614
- });
615
- }
616
- for (let i = 0; i < files.length; i += FILE_IO_BATCH_SIZE) {
617
- if (signal?.aborted) {
618
- if (parseWorker)
619
- parseWorker.terminate().catch(() => { });
620
- return {
621
- success: false,
622
- filesIndexed,
623
- filesSkipped,
624
- filesErrored,
625
- nodesCreated: totalNodes,
626
- edgesCreated: totalEdges,
627
- errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
628
- durationMs: Date.now() - startTime,
629
- };
630
- }
631
- const batch = files.slice(i, i + FILE_IO_BATCH_SIZE);
632
- // Read files in parallel (with path validation before any I/O)
633
- const fileContents = await Promise.all(batch.map(async (fp) => {
634
- try {
635
- const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, fp);
636
- if (!fullPath) {
637
- (0, errors_1.logWarn)('Path traversal blocked in batch reader', { filePath: fp });
638
- return { filePath: fp, content: null, stats: null, error: new Error('Path traversal blocked') };
639
- }
640
- const content = await fsp.readFile(fullPath, 'utf-8');
641
- const stats = await fsp.stat(fullPath);
642
- return { filePath: fp, content, stats, error: null };
643
- }
644
- catch (err) {
645
- return { filePath: fp, content: null, stats: null, error: err };
646
- }
647
- }));
648
- // Send to worker for parsing, store results on main thread
649
- for (const { filePath, content, stats, error } of fileContents) {
650
- if (signal?.aborted) {
651
- if (parseWorker)
652
- parseWorker.terminate().catch(() => { });
653
- return {
654
- success: false,
655
- filesIndexed,
656
- filesSkipped,
657
- filesErrored,
658
- nodesCreated: totalNodes,
659
- edgesCreated: totalEdges,
660
- errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
661
- durationMs: Date.now() - startTime,
662
- };
663
- }
664
- // Report progress before parsing (show current file being worked on)
665
- onProgress?.({
666
- phase: 'parsing',
667
- current: processed,
668
- total,
669
- currentFile: filePath,
670
- });
671
- if (error || content === null || stats === null) {
672
- processed++;
673
- filesErrored++;
674
- errors.push({
675
- message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
676
- filePath,
677
- severity: 'error',
678
- code: 'read_error',
679
- });
680
- continue;
681
- }
682
- // Honour MAX_FILE_SIZE. Without this check, vendored generated
683
- // headers, minified bundles, and other multi-MB files get indexed,
684
- // wasting WASM heap and the worker recycle budget on inputs with no
685
- // useful symbols. The single-file extractFile path already enforces
686
- // this; the bulk path used to silently skip the check.
687
- if (stats.size > MAX_FILE_SIZE) {
688
- processed++;
689
- filesSkipped++;
690
- errors.push({
691
- message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
692
- filePath,
693
- severity: 'warning',
694
- code: 'size_exceeded',
695
- });
696
- onProgress?.({ phase: 'parsing', current: processed, total });
697
- continue;
698
- }
699
- // Parse in worker thread (main thread stays unblocked).
700
- // Wrapped in try/catch to handle worker timeouts and crashes gracefully.
701
- let result;
702
- try {
703
- result = await requestParse(filePath, content);
704
- }
705
- catch (parseErr) {
706
- processed++;
707
- filesErrored++;
708
- errors.push({
709
- message: parseErr instanceof Error ? parseErr.message : String(parseErr),
710
- filePath,
711
- severity: 'error',
712
- code: 'parse_error',
713
- });
714
- continue;
715
- }
716
- processed++;
717
- // Store in database on main thread (SQLite is not thread-safe)
718
- if (result.nodes.length > 0 || result.errors.length === 0) {
719
- const language = (0, grammars_1.detectLanguage)(filePath, content);
720
- this.storeExtractionResult(filePath, content, language, stats, result);
721
- }
722
- if (result.errors.length > 0) {
723
- for (const err of result.errors) {
724
- if (!err.filePath)
725
- err.filePath = filePath;
726
- }
727
- errors.push(...result.errors);
728
- }
729
- if (result.nodes.length > 0) {
730
- filesIndexed++;
731
- totalNodes += result.nodes.length;
732
- totalEdges += result.edges.length;
733
- }
734
- else if (result.errors.some((e) => e.severity === 'error')) {
735
- filesErrored++;
736
- }
737
- else {
738
- filesSkipped++;
739
- }
740
- }
741
- }
742
- // Report 100% so the progress bar doesn't hang at 99%
743
- onProgress?.({
744
- phase: 'parsing',
745
- current: total,
746
- total,
747
- });
748
- // Yield so the shimmer worker's buffered stdout writes can flush.
749
- // Worker thread stdout is proxied through the main thread's event loop,
750
- // so synchronous work here blocks the animation from rendering.
751
- await new Promise(resolve => setImmediate(resolve));
752
- // Retry pass: files that failed due to WASM memory corruption may succeed
753
- // on a fresh worker with a clean heap. Recycle before each attempt so
754
- // every file gets the absolute cleanest WASM state possible.
755
- const retryableErrors = errors.filter((e) => e.code === 'parse_error' && e.filePath &&
756
- (e.message.includes('Worker exited') || e.message.includes('memory access out of bounds')));
757
- if (retryableErrors.length > 0 && WorkerClass) {
758
- log(`Retrying ${retryableErrors.length} files that failed due to WASM memory errors...`);
759
- const stillFailing = [];
760
- for (const errEntry of retryableErrors) {
761
- const filePath = errEntry.filePath;
762
- if (signal?.aborted)
763
- break;
764
- // Fresh worker for every retry — maximum WASM headroom
765
- recycleWorker();
766
- let content;
767
- try {
768
- const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
769
- if (!fullPath)
770
- continue;
771
- content = await fsp.readFile(fullPath, 'utf-8');
772
- }
773
- catch {
774
- continue;
775
- }
776
- let result;
777
- try {
778
- result = await requestParse(filePath, content);
779
- }
780
- catch {
781
- stillFailing.push(errEntry);
782
- continue;
783
- }
784
- if (result.nodes.length > 0 || result.errors.length === 0) {
785
- const language = (0, grammars_1.detectLanguage)(filePath, content);
786
- const stats = await fsp.stat(path.join(this.rootDir, filePath));
787
- this.storeExtractionResult(filePath, content, language, stats, result);
788
- const idx = errors.indexOf(errEntry);
789
- if (idx >= 0)
790
- errors.splice(idx, 1);
791
- filesErrored--;
792
- filesIndexed++;
793
- totalNodes += result.nodes.length;
794
- totalEdges += result.edges.length;
795
- log(`Retry OK: ${filePath} (${result.nodes.length} nodes)`);
796
- }
797
- }
798
- // Last resort: for files that still crash on a clean worker, strip
799
- // comment-only lines to reduce WASM memory pressure. Many compiler
800
- // test files are 90%+ comments (CHECK directives) that don't contribute
801
- // code nodes but consume parser memory.
802
- if (stillFailing.length > 0) {
803
- log(`${stillFailing.length} files still failing — retrying with comments stripped...`);
804
- for (const errEntry of stillFailing) {
805
- const filePath = errEntry.filePath;
806
- if (signal?.aborted)
807
- break;
808
- recycleWorker();
809
- let fullContent;
810
- try {
811
- const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, filePath);
812
- if (!fullPath)
813
- continue;
814
- fullContent = await fsp.readFile(fullPath, 'utf-8');
815
- }
816
- catch {
817
- continue;
818
- }
819
- // Strip lines that are entirely comments (preserving line numbers
820
- // by replacing with empty lines so node positions stay correct)
821
- const stripped = fullContent
822
- .split('\n')
823
- .map(line => /^\s*\/\//.test(line) ? '' : line)
824
- .join('\n');
825
- let result;
826
- try {
827
- result = await requestParse(filePath, stripped);
828
- }
829
- catch {
830
- continue;
831
- }
832
- if (result.nodes.length > 0 || result.errors.length === 0) {
833
- const language = (0, grammars_1.detectLanguage)(filePath, fullContent);
834
- const stats = await fsp.stat(path.join(this.rootDir, filePath));
835
- this.storeExtractionResult(filePath, fullContent, language, stats, result);
836
- const idx = errors.indexOf(errEntry);
837
- if (idx >= 0)
838
- errors.splice(idx, 1);
839
- filesErrored--;
840
- filesIndexed++;
841
- totalNodes += result.nodes.length;
842
- totalEdges += result.edges.length;
843
- log(`Retry (stripped) OK: ${filePath} (${result.nodes.length} nodes)`);
844
- }
845
- }
846
- }
847
- }
848
- // Shut down parse worker and clear any pending timers
849
- rejectAllPending('Indexing complete');
850
- if (parseWorker) {
851
- parseWorker.terminate().catch(() => { });
852
- }
853
- return {
854
- success: filesIndexed > 0 || errors.filter((e) => e.severity === 'error').length === 0,
855
- filesIndexed,
856
- filesSkipped,
857
- filesErrored,
858
- nodesCreated: totalNodes,
859
- edgesCreated: totalEdges,
860
- errors,
861
- durationMs: Date.now() - startTime,
862
- };
863
- }
864
- /**
865
- * Index specific files
866
- */
867
- async indexFiles(filePaths) {
868
- const startTime = Date.now();
869
- const errors = [];
870
- let filesIndexed = 0;
871
- let filesSkipped = 0;
872
- let filesErrored = 0;
873
- let totalNodes = 0;
874
- let totalEdges = 0;
875
- for (const filePath of filePaths) {
876
- const result = await this.indexFile(filePath);
877
- if (result.errors.length > 0) {
878
- errors.push(...result.errors);
879
- }
880
- if (result.nodes.length > 0) {
881
- filesIndexed++;
882
- totalNodes += result.nodes.length;
883
- totalEdges += result.edges.length;
884
- }
885
- else if (result.errors.some((e) => e.severity === 'error')) {
886
- filesErrored++;
887
- }
888
- else {
889
- filesSkipped++;
890
- }
891
- }
892
- return {
893
- success: filesIndexed > 0 || errors.filter((e) => e.severity === 'error').length === 0,
894
- filesIndexed,
895
- filesSkipped,
896
- filesErrored,
897
- nodesCreated: totalNodes,
898
- edgesCreated: totalEdges,
899
- errors,
900
- durationMs: Date.now() - startTime,
901
- };
902
- }
903
- /**
904
- * Index a single file
905
- */
906
- async indexFile(relativePath) {
907
- const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, relativePath);
908
- if (!fullPath) {
909
- return {
910
- nodes: [],
911
- edges: [],
912
- unresolvedReferences: [],
913
- errors: [{ message: `Path traversal blocked: ${relativePath}`, filePath: relativePath, severity: 'error', code: 'path_traversal' }],
914
- durationMs: 0,
915
- };
916
- }
917
- // Read file content and stats
918
- let content;
919
- let stats;
920
- try {
921
- stats = await fsp.stat(fullPath);
922
- content = await fsp.readFile(fullPath, 'utf-8');
923
- }
924
- catch (error) {
925
- return {
926
- nodes: [],
927
- edges: [],
928
- unresolvedReferences: [],
929
- errors: [
930
- {
931
- message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
932
- filePath: relativePath,
933
- severity: 'error',
934
- code: 'read_error',
935
- },
936
- ],
937
- durationMs: 0,
938
- };
939
- }
940
- return this.indexFileWithContent(relativePath, content, stats);
941
- }
942
- /**
943
- * Index a single file with pre-read content and stats.
944
- * Used by the parallel batch reader to avoid redundant file I/O.
945
- */
946
- async indexFileWithContent(relativePath, content, stats) {
947
- // Prevent path traversal
948
- const fullPath = (0, utils_1.validatePathWithinRoot)(this.rootDir, relativePath);
949
- if (!fullPath) {
950
- (0, errors_1.logWarn)('Path traversal blocked in indexFileWithContent', { relativePath });
951
- return {
952
- nodes: [],
953
- edges: [],
954
- unresolvedReferences: [],
955
- errors: [{ message: 'Path traversal blocked', filePath: relativePath, severity: 'error', code: 'path_traversal' }],
956
- durationMs: 0,
957
- };
958
- }
959
- // Check file size
960
- if (stats.size > MAX_FILE_SIZE) {
961
- return {
962
- nodes: [],
963
- edges: [],
964
- unresolvedReferences: [],
965
- errors: [
966
- {
967
- message: `File exceeds max size (${stats.size} > ${MAX_FILE_SIZE})`,
968
- filePath: relativePath,
969
- severity: 'warning',
970
- code: 'size_exceeded',
971
- },
972
- ],
973
- durationMs: 0,
974
- };
975
- }
976
- // Detect language
977
- const language = (0, grammars_1.detectLanguage)(relativePath, content);
978
- if (!(0, grammars_1.isLanguageSupported)(language)) {
979
- return {
980
- nodes: [],
981
- edges: [],
982
- unresolvedReferences: [],
983
- errors: [],
984
- durationMs: 0,
985
- };
986
- }
987
- // Extract from source. Use cached framework names if indexAll has run,
988
- // otherwise detect on the spot so single-file re-index paths still emit
989
- // route nodes / middleware / etc.
990
- const frameworkNames = this.ensureDetectedFrameworks();
991
- const result = (0, tree_sitter_1.extractFromSource)(relativePath, content, language, frameworkNames);
992
- // Store in database
993
- if (result.nodes.length > 0 || result.errors.length === 0) {
994
- this.storeExtractionResult(relativePath, content, language, stats, result);
995
- }
996
- return result;
997
- }
998
- /**
999
- * Store extraction result in database
1000
- */
1001
- storeExtractionResult(filePath, content, language, stats, result) {
1002
- const contentHash = hashContent(content);
1003
- // Check if file already exists and hasn't changed
1004
- const existingFile = this.queries.getFileByPath(filePath);
1005
- if (existingFile && existingFile.contentHash === contentHash) {
1006
- return; // No changes
1007
- }
1008
- // Delete existing data for this file
1009
- if (existingFile) {
1010
- this.queries.deleteFile(filePath);
1011
- }
1012
- // Filter out nodes with missing required fields before insertion.
1013
- // This prevents FK violations when edges reference nodes that would
1014
- // be silently skipped by insertNode() (see issue #42).
1015
- const validNodes = result.nodes.filter((n) => n.id && n.kind && n.name && n.filePath && n.language);
1016
- // Insert nodes
1017
- if (validNodes.length > 0) {
1018
- this.queries.insertNodes(validNodes);
1019
- }
1020
- // Filter edges to only reference nodes that were actually inserted
1021
- if (result.edges.length > 0) {
1022
- const insertedIds = new Set(validNodes.map((n) => n.id));
1023
- const validEdges = result.edges.filter((e) => insertedIds.has(e.source) && insertedIds.has(e.target));
1024
- if (validEdges.length > 0) {
1025
- this.queries.insertEdges(validEdges);
1026
- }
1027
- }
1028
- // Insert unresolved references in batch with denormalized filePath/language
1029
- if (result.unresolvedReferences.length > 0) {
1030
- const insertedIds = new Set(validNodes.map((n) => n.id));
1031
- const refsWithContext = result.unresolvedReferences
1032
- .filter((ref) => insertedIds.has(ref.fromNodeId) && ref.referenceName != null && ref.referenceName !== '')
1033
- .map((ref) => ({
1034
- ...ref,
1035
- filePath: ref.filePath ?? filePath,
1036
- language: ref.language ?? language,
1037
- }));
1038
- if (refsWithContext.length > 0) {
1039
- this.queries.insertUnresolvedRefsBatch(refsWithContext);
1040
- }
1041
- }
1042
- // Insert file record
1043
- const fileRecord = {
1044
- path: filePath,
1045
- contentHash,
1046
- language,
1047
- size: stats.size,
1048
- modifiedAt: stats.mtimeMs,
1049
- indexedAt: Date.now(),
1050
- nodeCount: result.nodes.length,
1051
- errors: result.errors.length > 0 ? result.errors : undefined,
1052
- };
1053
- this.queries.upsertFile(fileRecord);
1054
- }
1055
- /**
1056
- * Sync with current file state.
1057
- * Uses git status as a fast path when available, falling back to full scan.
1058
- */
1059
- async sync(onProgress) {
1060
- await (0, grammars_1.initGrammars)(); // Initialize WASM runtime (grammars loaded lazily below)
1061
- const startTime = Date.now();
1062
- let filesChecked = 0;
1063
- let filesAdded = 0;
1064
- let filesModified = 0;
1065
- let filesRemoved = 0;
1066
- let nodesUpdated = 0;
1067
- const changedFilePaths = [];
1068
- onProgress?.({
1069
- phase: 'scanning',
1070
- current: 0,
1071
- total: 0,
1072
- });
1073
- const filesToIndex = [];
1074
- const gitChanges = getGitChangedFiles(this.rootDir);
1075
- if (gitChanges) {
1076
- // === Git fast path ===
1077
- // Only inspect the files git reports as changed instead of scanning everything.
1078
- filesChecked = gitChanges.modified.length + gitChanges.added.length + gitChanges.deleted.length;
1079
- // Handle deleted files
1080
- for (const filePath of gitChanges.deleted) {
1081
- const tracked = this.queries.getFileByPath(filePath);
1082
- if (tracked) {
1083
- this.queries.deleteFile(filePath);
1084
- filesRemoved++;
1085
- }
1086
- }
1087
- // Handle modified + added files — read + hash only these. Untracked
1088
- // (`??`) files stay untracked in git even after we index them, so they
1089
- // can't be trusted as "new": re-hash and compare against the DB exactly
1090
- // like modified files. Otherwise every sync re-indexes them and status
1091
- // reports them as pending forever. (See issue #206.)
1092
- for (const filePath of [...gitChanges.modified, ...gitChanges.added]) {
1093
- const fullPath = path.join(this.rootDir, filePath);
1094
- let content;
1095
- try {
1096
- content = fs.readFileSync(fullPath, 'utf-8');
1097
- }
1098
- catch (error) {
1099
- (0, errors_1.logDebug)('Skipping unreadable file during sync', { filePath, error: String(error) });
1100
- continue;
1101
- }
1102
- const contentHash = hashContent(content);
1103
- const tracked = this.queries.getFileByPath(filePath);
1104
- if (!tracked) {
1105
- filesToIndex.push(filePath);
1106
- changedFilePaths.push(filePath);
1107
- filesAdded++;
1108
- }
1109
- else if (tracked.contentHash !== contentHash) {
1110
- filesToIndex.push(filePath);
1111
- changedFilePaths.push(filePath);
1112
- filesModified++;
1113
- }
1114
- }
1115
- }
1116
- else {
1117
- // === Fallback: full scan (non-git project or git failure) ===
1118
- const currentFiles = new Set(scanDirectory(this.rootDir));
1119
- filesChecked = currentFiles.size;
1120
- // Build Map for O(1) lookups instead of .find() per file
1121
- const trackedFiles = this.queries.getAllFiles();
1122
- const trackedMap = new Map();
1123
- for (const f of trackedFiles) {
1124
- trackedMap.set(f.path, f);
1125
- }
1126
- // Find files to remove (in DB but not on disk)
1127
- for (const tracked of trackedFiles) {
1128
- if (!currentFiles.has(tracked.path)) {
1129
- this.queries.deleteFile(tracked.path);
1130
- filesRemoved++;
1131
- }
1132
- }
1133
- // Find files to add or update
1134
- for (const filePath of currentFiles) {
1135
- const fullPath = path.join(this.rootDir, filePath);
1136
- let content;
1137
- try {
1138
- content = fs.readFileSync(fullPath, 'utf-8');
1139
- }
1140
- catch (error) {
1141
- (0, errors_1.logDebug)('Skipping unreadable file during sync', { filePath, error: String(error) });
1142
- continue;
1143
- }
1144
- const contentHash = hashContent(content);
1145
- const tracked = trackedMap.get(filePath);
1146
- if (!tracked) {
1147
- filesToIndex.push(filePath);
1148
- changedFilePaths.push(filePath);
1149
- filesAdded++;
1150
- }
1151
- else if (tracked.contentHash !== contentHash) {
1152
- filesToIndex.push(filePath);
1153
- changedFilePaths.push(filePath);
1154
- filesModified++;
1155
- }
1156
- }
1157
- }
1158
- // Load only grammars needed for changed files
1159
- if (filesToIndex.length > 0) {
1160
- const neededLanguages = [...new Set(filesToIndex.map((f) => (0, grammars_1.detectLanguage)(f)))];
1161
- // .h files default to 'c' but may be C++ — ensure cpp grammar is loaded
1162
- if (neededLanguages.includes('c') && !neededLanguages.includes('cpp')) {
1163
- neededLanguages.push('cpp');
1164
- }
1165
- await (0, grammars_1.loadGrammarsForLanguages)(neededLanguages);
1166
- }
1167
- // Index changed files
1168
- const total = filesToIndex.length;
1169
- for (let i = 0; i < filesToIndex.length; i++) {
1170
- const filePath = filesToIndex[i];
1171
- onProgress?.({
1172
- phase: 'parsing',
1173
- current: i + 1,
1174
- total,
1175
- currentFile: filePath,
1176
- });
1177
- const result = await this.indexFile(filePath);
1178
- nodesUpdated += result.nodes.length;
1179
- }
1180
- return {
1181
- filesChecked,
1182
- filesAdded,
1183
- filesModified,
1184
- filesRemoved,
1185
- nodesUpdated,
1186
- durationMs: Date.now() - startTime,
1187
- changedFilePaths: changedFilePaths.length > 0 ? changedFilePaths : undefined,
1188
- };
1189
- }
1190
- /**
1191
- * Get files that have changed since last index.
1192
- * Uses git status as a fast path when available, falling back to full scan.
1193
- */
1194
- getChangedFiles() {
1195
- const gitChanges = getGitChangedFiles(this.rootDir);
1196
- if (gitChanges) {
1197
- // === Git fast path ===
1198
- const added = [];
1199
- const modified = [];
1200
- const removed = [];
1201
- // Deleted files — only report if tracked in DB
1202
- for (const filePath of gitChanges.deleted) {
1203
- const tracked = this.queries.getFileByPath(filePath);
1204
- if (tracked) {
1205
- removed.push(filePath);
1206
- }
1207
- }
1208
- // Modified + added files — read + hash, compare with DB. Untracked (`??`)
1209
- // files stay untracked in git even after indexing, so they must be
1210
- // hash-compared like modified files instead of always counting as added —
1211
- // otherwise status reports them as pending forever. (See issue #206.)
1212
- for (const filePath of [...gitChanges.modified, ...gitChanges.added]) {
1213
- const fullPath = path.join(this.rootDir, filePath);
1214
- let content;
1215
- try {
1216
- content = fs.readFileSync(fullPath, 'utf-8');
1217
- }
1218
- catch (error) {
1219
- (0, errors_1.logDebug)('Skipping unreadable file while detecting changes', { filePath, error: String(error) });
1220
- continue;
1221
- }
1222
- const contentHash = hashContent(content);
1223
- const tracked = this.queries.getFileByPath(filePath);
1224
- if (!tracked) {
1225
- added.push(filePath);
1226
- }
1227
- else if (tracked.contentHash !== contentHash) {
1228
- modified.push(filePath);
1229
- }
1230
- }
1231
- return { added, modified, removed };
1232
- }
1233
- // === Fallback: full scan (non-git project or git failure) ===
1234
- const currentFiles = new Set(scanDirectory(this.rootDir));
1235
- const trackedFiles = this.queries.getAllFiles();
1236
- // Build Map for O(1) lookups
1237
- const trackedMap = new Map();
1238
- for (const f of trackedFiles) {
1239
- trackedMap.set(f.path, f);
1240
- }
1241
- const added = [];
1242
- const modified = [];
1243
- const removed = [];
1244
- // Find removed files
1245
- for (const tracked of trackedFiles) {
1246
- if (!currentFiles.has(tracked.path)) {
1247
- removed.push(tracked.path);
1248
- }
1249
- }
1250
- // Find added and modified files
1251
- for (const filePath of currentFiles) {
1252
- const fullPath = path.join(this.rootDir, filePath);
1253
- let content;
1254
- try {
1255
- content = fs.readFileSync(fullPath, 'utf-8');
1256
- }
1257
- catch (error) {
1258
- (0, errors_1.logDebug)('Skipping unreadable file while detecting changes', { filePath, error: String(error) });
1259
- continue;
1260
- }
1261
- const contentHash = hashContent(content);
1262
- const tracked = trackedMap.get(filePath);
1263
- if (!tracked) {
1264
- added.push(filePath);
1265
- }
1266
- else if (tracked.contentHash !== contentHash) {
1267
- modified.push(filePath);
1268
- }
1269
- }
1270
- return { added, modified, removed };
1271
- }
1272
- }
1273
- exports.ExtractionOrchestrator = ExtractionOrchestrator;
1274
- // Re-export useful types and functions
1275
- var tree_sitter_2 = require("./tree-sitter");
1276
- Object.defineProperty(exports, "extractFromSource", { enumerable: true, get: function () { return tree_sitter_2.extractFromSource; } });
1277
- var grammars_2 = require("./grammars");
1278
- Object.defineProperty(exports, "detectLanguage", { enumerable: true, get: function () { return grammars_2.detectLanguage; } });
1279
- Object.defineProperty(exports, "isSourceFile", { enumerable: true, get: function () { return grammars_2.isSourceFile; } });
1280
- Object.defineProperty(exports, "isLanguageSupported", { enumerable: true, get: function () { return grammars_2.isLanguageSupported; } });
1281
- Object.defineProperty(exports, "isGrammarLoaded", { enumerable: true, get: function () { return grammars_2.isGrammarLoaded; } });
1282
- Object.defineProperty(exports, "getSupportedLanguages", { enumerable: true, get: function () { return grammars_2.getSupportedLanguages; } });
1283
- Object.defineProperty(exports, "initGrammars", { enumerable: true, get: function () { return grammars_2.initGrammars; } });
1284
- Object.defineProperty(exports, "loadGrammarsForLanguages", { enumerable: true, get: function () { return grammars_2.loadGrammarsForLanguages; } });
1285
- Object.defineProperty(exports, "loadAllGrammars", { enumerable: true, get: function () { return grammars_2.loadAllGrammars; } });
1286
- //# sourceMappingURL=index.js.map