@unerr-ai/unerr 0.2.0 → 0.2.2

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 (363) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +37236 -35793
  3. package/package.json +6 -1
  4. package/dist/behaviors/agent-llm-bridge.js +0 -166
  5. package/dist/behaviors/architecture-guard.js +0 -256
  6. package/dist/behaviors/auto-doc.js +0 -247
  7. package/dist/behaviors/cascade-guard.js +0 -289
  8. package/dist/behaviors/change-narrative.js +0 -270
  9. package/dist/behaviors/convention-drift.js +0 -290
  10. package/dist/behaviors/framework.js +0 -235
  11. package/dist/behaviors/guard-formatter.js +0 -44
  12. package/dist/behaviors/incomplete-work.js +0 -270
  13. package/dist/behaviors/loop-breaker.js +0 -300
  14. package/dist/behaviors/session-continuity.js +0 -208
  15. package/dist/commands/branches.js +0 -97
  16. package/dist/commands/check-commit.js +0 -225
  17. package/dist/commands/compress-output.js +0 -64
  18. package/dist/commands/config-verify.js +0 -243
  19. package/dist/commands/daemon.js +0 -905
  20. package/dist/commands/dashboard.js +0 -52
  21. package/dist/commands/debug.js +0 -200
  22. package/dist/commands/enrich.js +0 -184
  23. package/dist/commands/exec.js +0 -233
  24. package/dist/commands/gain.js +0 -156
  25. package/dist/commands/hook.js +0 -88
  26. package/dist/commands/index.js +0 -88
  27. package/dist/commands/init.js +0 -74
  28. package/dist/commands/install.js +0 -505
  29. package/dist/commands/learn.js +0 -116
  30. package/dist/commands/manifest.js +0 -193
  31. package/dist/commands/rewind.js +0 -103
  32. package/dist/commands/serve.js +0 -19
  33. package/dist/commands/setup-wizard.js +0 -414
  34. package/dist/commands/skills.js +0 -64
  35. package/dist/commands/stats.js +0 -20
  36. package/dist/commands/status.js +0 -654
  37. package/dist/commands/timeline.js +0 -139
  38. package/dist/commands/uninstall.js +0 -230
  39. package/dist/components/App.js +0 -109
  40. package/dist/components/Banner.js +0 -12
  41. package/dist/components/ConfirmPrompt.js +0 -25
  42. package/dist/components/DriftSummary.js +0 -23
  43. package/dist/components/GradeBadge.js +0 -15
  44. package/dist/components/HealthCard.js +0 -18
  45. package/dist/components/InkSpinner.js +0 -22
  46. package/dist/components/InputBox.js +0 -17
  47. package/dist/components/KeyValue.js +0 -13
  48. package/dist/components/MessageList.js +0 -14
  49. package/dist/components/ProgressBar.js +0 -26
  50. package/dist/components/Section.js +0 -16
  51. package/dist/components/SessionSummaryCard.js +0 -73
  52. package/dist/components/StartupDisplay.js +0 -24
  53. package/dist/components/StatusDashboard.js +0 -57
  54. package/dist/components/StatusLine.js +0 -8
  55. package/dist/components/StepLine.js +0 -22
  56. package/dist/components/Theme.js +0 -20
  57. package/dist/components/ToolProgress.js +0 -8
  58. package/dist/components/ViolationList.js +0 -21
  59. package/dist/components/render.js +0 -13
  60. package/dist/config/agent-registry.js +0 -237
  61. package/dist/config/claude-settings-hooks.js +0 -304
  62. package/dist/config/hook-installer.js +0 -65
  63. package/dist/config/instruction-writer.js +0 -388
  64. package/dist/config/mcp-config-writer.js +0 -266
  65. package/dist/config/settings.js +0 -174
  66. package/dist/config/tool-detector.js +0 -42
  67. package/dist/config/value-surfacing.js +0 -119
  68. package/dist/core/context-assembly.js +0 -108
  69. package/dist/core/conversation.js +0 -33
  70. package/dist/core/local-chat-provider.js +0 -475
  71. package/dist/core/provider-factory.js +0 -55
  72. package/dist/core/providers.js +0 -90
  73. package/dist/core/query-engine.js +0 -174
  74. package/dist/daemon/api.js +0 -312
  75. package/dist/daemon/autostart.js +0 -119
  76. package/dist/daemon/bootstrap.js +0 -39
  77. package/dist/daemon/client.js +0 -164
  78. package/dist/daemon/detect-ci.js +0 -81
  79. package/dist/daemon/platform-linux.js +0 -146
  80. package/dist/daemon/platform-macos.js +0 -134
  81. package/dist/daemon/platform-windows.js +0 -116
  82. package/dist/daemon/process-manager.js +0 -299
  83. package/dist/daemon/protocol.js +0 -23
  84. package/dist/daemon/registry.js +0 -270
  85. package/dist/daemon/settings-schema.js +0 -72
  86. package/dist/daemon/system-health.js +0 -134
  87. package/dist/daemon/version-checker.js +0 -262
  88. package/dist/daemon/warm-start.js +0 -223
  89. package/dist/entrypoints/cli.js +0 -1043
  90. package/dist/entrypoints/daemon.js +0 -380
  91. package/dist/entrypoints/repl.js +0 -147
  92. package/dist/hooks/adapters/claude-code.js +0 -90
  93. package/dist/hooks/adapters/cline.js +0 -100
  94. package/dist/hooks/adapters/cursor.js +0 -98
  95. package/dist/hooks/hook-dedup.js +0 -79
  96. package/dist/hooks/hook-runner.js +0 -113
  97. package/dist/hooks/navigation-hooks.js +0 -175
  98. package/dist/hooks/prompt-hooks.js +0 -63
  99. package/dist/hooks/shell-hooks.js +0 -47
  100. package/dist/ignore.js +0 -111
  101. package/dist/intelligence/approach-suggester.js +0 -61
  102. package/dist/intelligence/ast-extractor.js +0 -2615
  103. package/dist/intelligence/ast-worker.js +0 -34
  104. package/dist/intelligence/background-indexer.js +0 -121
  105. package/dist/intelligence/blast-radius.js +0 -200
  106. package/dist/intelligence/community-detection.js +0 -691
  107. package/dist/intelligence/community-detector.js +0 -184
  108. package/dist/intelligence/computation-scheduler.js +0 -75
  109. package/dist/intelligence/confidence-propagation.js +0 -47
  110. package/dist/intelligence/convention-detector.js +0 -242
  111. package/dist/intelligence/convention-learner.js +0 -205
  112. package/dist/intelligence/convention-matcher.js +0 -205
  113. package/dist/intelligence/cozo-schema.js +0 -376
  114. package/dist/intelligence/decision-point-detector.js +0 -90
  115. package/dist/intelligence/deep-dive-tools.js +0 -586
  116. package/dist/intelligence/durability-scorer.js +0 -84
  117. package/dist/intelligence/exploration-cost.js +0 -204
  118. package/dist/intelligence/exploration-pattern-tracker.js +0 -61
  119. package/dist/intelligence/fact-generator.js +0 -322
  120. package/dist/intelligence/facts-schema.js +0 -90
  121. package/dist/intelligence/file-intelligence.js +0 -59
  122. package/dist/intelligence/graph-holder.js +0 -220
  123. package/dist/intelligence/graph-temporal-joiner.js +0 -238
  124. package/dist/intelligence/health-grade.js +0 -423
  125. package/dist/intelligence/health-grader.js +0 -200
  126. package/dist/intelligence/health-map-data.js +0 -259
  127. package/dist/intelligence/import-symbols.js +0 -136
  128. package/dist/intelligence/incremental-indexer.js +0 -658
  129. package/dist/intelligence/indexer/centrality.js +0 -62
  130. package/dist/intelligence/indexer/cfg-context.js +0 -95
  131. package/dist/intelligence/indexer/confidence.js +0 -34
  132. package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
  133. package/dist/intelligence/indexer/edge-repair.js +0 -89
  134. package/dist/intelligence/indexer/entity-key.js +0 -17
  135. package/dist/intelligence/indexer/export-map.js +0 -132
  136. package/dist/intelligence/indexer/git-cochange.js +0 -128
  137. package/dist/intelligence/indexer/graph-patch.js +0 -147
  138. package/dist/intelligence/indexer/incremental.js +0 -78
  139. package/dist/intelligence/indexer/ingest.js +0 -160
  140. package/dist/intelligence/indexer/language-detect.js +0 -226
  141. package/dist/intelligence/indexer/metadata.js +0 -63
  142. package/dist/intelligence/indexer/mutation-tracker.js +0 -79
  143. package/dist/intelligence/indexer/orchestrator.js +0 -155
  144. package/dist/intelligence/indexer/plugin-interface.js +0 -31
  145. package/dist/intelligence/indexer/plugins/csharp.js +0 -440
  146. package/dist/intelligence/indexer/plugins/go.js +0 -335
  147. package/dist/intelligence/indexer/plugins/java.js +0 -370
  148. package/dist/intelligence/indexer/plugins/python.js +0 -358
  149. package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
  150. package/dist/intelligence/indexer/plugins/ruby.js +0 -290
  151. package/dist/intelligence/indexer/plugins/rust.js +0 -484
  152. package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
  153. package/dist/intelligence/indexer/plugins/typescript.js +0 -456
  154. package/dist/intelligence/indexer/resource-monitor.js +0 -93
  155. package/dist/intelligence/indexer/scip/decoder.js +0 -253
  156. package/dist/intelligence/indexer/scip/detector.js +0 -232
  157. package/dist/intelligence/indexer/scip/downloader.js +0 -427
  158. package/dist/intelligence/indexer/scip/fallback.js +0 -34
  159. package/dist/intelligence/indexer/scip/merger.js +0 -109
  160. package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
  161. package/dist/intelligence/indexer/scip/runner.js +0 -98
  162. package/dist/intelligence/indexer/snapshot.js +0 -66
  163. package/dist/intelligence/indexer/test-detector.js +0 -196
  164. package/dist/intelligence/indexer/watch-integration.js +0 -61
  165. package/dist/intelligence/indexer/worker.js +0 -85
  166. package/dist/intelligence/local-convention-detector.js +0 -437
  167. package/dist/intelligence/local-embeddings.js +0 -190
  168. package/dist/intelligence/local-graph.js +0 -1946
  169. package/dist/intelligence/local-indexer.js +0 -1575
  170. package/dist/intelligence/local-llm.js +0 -163
  171. package/dist/intelligence/local-rule-generator.js +0 -154
  172. package/dist/intelligence/local-snapshot.js +0 -213
  173. package/dist/intelligence/negative-knowledge.js +0 -103
  174. package/dist/intelligence/persistent-db.js +0 -85
  175. package/dist/intelligence/query-router.js +0 -2556
  176. package/dist/intelligence/risk-classifier.js +0 -116
  177. package/dist/intelligence/rule-evaluator.js +0 -380
  178. package/dist/intelligence/rule-generator.js +0 -49
  179. package/dist/intelligence/search-index.js +0 -173
  180. package/dist/intelligence/semantic/docstring-extractor.js +0 -67
  181. package/dist/intelligence/semantic/embedding-store.js +0 -52
  182. package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
  183. package/dist/intelligence/semantic/git-message-miner.js +0 -114
  184. package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
  185. package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
  186. package/dist/intelligence/semantic/node2vec-walks.js +0 -103
  187. package/dist/intelligence/semantic/path-domain-inference.js +0 -112
  188. package/dist/intelligence/semantic/similarity-engine.js +0 -60
  189. package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
  190. package/dist/intelligence/session-brief-builder.js +0 -159
  191. package/dist/intelligence/session-context.js +0 -221
  192. package/dist/intelligence/session-health-monitor.js +0 -211
  193. package/dist/intelligence/session-narrative.js +0 -197
  194. package/dist/intelligence/session-pattern-analyzer.js +0 -218
  195. package/dist/intelligence/signal-scorer.js +0 -390
  196. package/dist/intelligence/signal-show-store.js +0 -182
  197. package/dist/intelligence/smart-truncate.js +0 -158
  198. package/dist/intelligence/subgraph-cache.js +0 -88
  199. package/dist/intelligence/temporal-facts.js +0 -494
  200. package/dist/intelligence/token-estimator.js +0 -100
  201. package/dist/intelligence/tool-injector.js +0 -87
  202. package/dist/intelligence/tree-sitter-loader.js +0 -71
  203. package/dist/intelligence/worker-pool.js +0 -116
  204. package/dist/proxy/arg-validator.js +0 -79
  205. package/dist/proxy/auto-bootstrap.js +0 -167
  206. package/dist/proxy/bridge.js +0 -147
  207. package/dist/proxy/budget-enforcer.js +0 -70
  208. package/dist/proxy/compression-quality-monitor.js +0 -160
  209. package/dist/proxy/compression-stats.js +0 -51
  210. package/dist/proxy/context-rot-detector.js +0 -137
  211. package/dist/proxy/drift-detector.js +0 -139
  212. package/dist/proxy/efficiency-tracker.js +0 -79
  213. package/dist/proxy/fact-ranking.js +0 -154
  214. package/dist/proxy/format-encoder.js +0 -266
  215. package/dist/proxy/http-transport.js +0 -90
  216. package/dist/proxy/lifecycle-actor.js +0 -55
  217. package/dist/proxy/lifecycle-machine.js +0 -187
  218. package/dist/proxy/log-tailer.js +0 -265
  219. package/dist/proxy/model-pricing.js +0 -98
  220. package/dist/proxy/network-firewall.js +0 -141
  221. package/dist/proxy/nudge-state.js +0 -93
  222. package/dist/proxy/output-compressor.js +0 -185
  223. package/dist/proxy/pid-lock.js +0 -291
  224. package/dist/proxy/proxy-context.js +0 -11
  225. package/dist/proxy/proxy.js +0 -2633
  226. package/dist/proxy/response-enrichment.js +0 -32
  227. package/dist/proxy/response-envelope.js +0 -313
  228. package/dist/proxy/session-dedup.js +0 -82
  229. package/dist/proxy/session-legend.js +0 -30
  230. package/dist/proxy/session-persistence.js +0 -210
  231. package/dist/proxy/session-resume.js +0 -94
  232. package/dist/proxy/session-stats.js +0 -513
  233. package/dist/proxy/shell-classifier.js +0 -1346
  234. package/dist/proxy/shell-compression-log.js +0 -93
  235. package/dist/proxy/shell-compressor.js +0 -390
  236. package/dist/proxy/shell-graph-boost.js +0 -202
  237. package/dist/proxy/shell-monitor-map.js +0 -18
  238. package/dist/proxy/shell-stats.js +0 -54
  239. package/dist/proxy/shell-strategies/cloud.js +0 -215
  240. package/dist/proxy/shell-strategies/diff.js +0 -159
  241. package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
  242. package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
  243. package/dist/proxy/shell-strategies/git-status.js +0 -177
  244. package/dist/proxy/shell-strategies/key-value.js +0 -193
  245. package/dist/proxy/shell-strategies/log-text.js +0 -154
  246. package/dist/proxy/shell-strategies/omni.js +0 -188
  247. package/dist/proxy/shell-strategies/progress.js +0 -55
  248. package/dist/proxy/shell-strategies/redact.js +0 -76
  249. package/dist/proxy/shell-strategies/structured.js +0 -241
  250. package/dist/proxy/shell-strategies/tabular.js +0 -243
  251. package/dist/proxy/shell-strategies/test-results-types.js +0 -13
  252. package/dist/proxy/shell-strategies/test-results.js +0 -784
  253. package/dist/proxy/shell-strategies/tree-paths.js +0 -144
  254. package/dist/proxy/shell-strategies/yaml.js +0 -182
  255. package/dist/proxy/shell-tee.js +0 -111
  256. package/dist/proxy/signal-dedup.js +0 -171
  257. package/dist/proxy/startup-renderer.js +0 -158
  258. package/dist/proxy/task-token-display.js +0 -38
  259. package/dist/proxy/token-counter.js +0 -61
  260. package/dist/proxy/tool-clusters.js +0 -273
  261. package/dist/proxy/tool-definitions.js +0 -525
  262. package/dist/proxy/transport-mux.js +0 -229
  263. package/dist/proxy/wire-cap.js +0 -268
  264. package/dist/rules/developer.mozilla.org.json +0 -9
  265. package/dist/rules/github.com.json +0 -21
  266. package/dist/schemas/api/skills.js +0 -19
  267. package/dist/schemas/common/errors.js +0 -7
  268. package/dist/schemas/common/headers.js +0 -5
  269. package/dist/schemas/entities/edge.js +0 -25
  270. package/dist/schemas/entities/entity.js +0 -22
  271. package/dist/schemas/entities/rule.js +0 -18
  272. package/dist/schemas/index.js +0 -14
  273. package/dist/server/event-bus.js +0 -59
  274. package/dist/server/http.js +0 -156
  275. package/dist/server/middleware.js +0 -70
  276. package/dist/server/routes/drift.js +0 -97
  277. package/dist/server/routes/intelligence.js +0 -1217
  278. package/dist/server/routes/reasoning-quality.js +0 -444
  279. package/dist/server/routes/session.js +0 -86
  280. package/dist/server/routes/stream.js +0 -120
  281. package/dist/server/routes/system.js +0 -73
  282. package/dist/server/routes/temporal.js +0 -170
  283. package/dist/server/routes/timeline.js +0 -232
  284. package/dist/server/routes/token-flow.js +0 -403
  285. package/dist/skills/effectiveness-tracker.js +0 -93
  286. package/dist/skills/local-pack.js +0 -380
  287. package/dist/skills/resolver.js +0 -495
  288. package/dist/state-detector.js +0 -83
  289. package/dist/timeline/intent-detector.js +0 -263
  290. package/dist/timeline/loop-miner.js +0 -140
  291. package/dist/timeline/open-threads.js +0 -49
  292. package/dist/timeline/signal-reinforcer.js +0 -62
  293. package/dist/timeline/timeline-bootstrap.js +0 -151
  294. package/dist/timeline/timeline-store.js +0 -618
  295. package/dist/tools/coding/bash.js +0 -49
  296. package/dist/tools/coding/file-edit.js +0 -72
  297. package/dist/tools/coding/file-outline.js +0 -227
  298. package/dist/tools/coding/file-read-protocol.js +0 -425
  299. package/dist/tools/coding/file-read.js +0 -35
  300. package/dist/tools/coding/file-write.js +0 -43
  301. package/dist/tools/coding/glob-tool.js +0 -109
  302. package/dist/tools/coding/grep.js +0 -162
  303. package/dist/tools/coding/index.js +0 -27
  304. package/dist/tools/intelligence/index.js +0 -269
  305. package/dist/tools/intelligence/record-fact.js +0 -48
  306. package/dist/tools/intelligence/timeline-markers.js +0 -130
  307. package/dist/tools/registry.js +0 -47
  308. package/dist/tools/types.js +0 -8
  309. package/dist/tracking/auto-snapshot-triggers.js +0 -246
  310. package/dist/tracking/branch-context.js +0 -115
  311. package/dist/tracking/branch-snapshot.js +0 -217
  312. package/dist/tracking/causal-bridge.js +0 -317
  313. package/dist/tracking/circuit-breaker.js +0 -147
  314. package/dist/tracking/commit-watcher.js +0 -114
  315. package/dist/tracking/context-ledger.js +0 -119
  316. package/dist/tracking/correction-detector.js +0 -324
  317. package/dist/tracking/drift-tracker.js +0 -874
  318. package/dist/tracking/durability-tracker.js +0 -94
  319. package/dist/tracking/entity-rewind.js +0 -200
  320. package/dist/tracking/file-hash-state.js +0 -114
  321. package/dist/tracking/git-attribution.js +0 -132
  322. package/dist/tracking/git-trailers.js +0 -171
  323. package/dist/tracking/intelligence-counter.js +0 -46
  324. package/dist/tracking/intent-correlator.js +0 -202
  325. package/dist/tracking/intent-encoder.js +0 -52
  326. package/dist/tracking/intent-token-tracker.js +0 -159
  327. package/dist/tracking/ledger-archiver.js +0 -94
  328. package/dist/tracking/ledger-chains.js +0 -245
  329. package/dist/tracking/metrics-store.js +0 -361
  330. package/dist/tracking/native-watcher.js +0 -131
  331. package/dist/tracking/offline-rewind.js +0 -295
  332. package/dist/tracking/pending-violations.js +0 -74
  333. package/dist/tracking/persistence-effectiveness.js +0 -167
  334. package/dist/tracking/prompt-durability.js +0 -202
  335. package/dist/tracking/quality-signals.js +0 -213
  336. package/dist/tracking/redactor.js +0 -73
  337. package/dist/tracking/rewind-engine.js +0 -161
  338. package/dist/tracking/session-history.js +0 -128
  339. package/dist/tracking/session-receipt.js +0 -88
  340. package/dist/tracking/session-summary-writer.js +0 -157
  341. package/dist/tracking/shadow-ledger.js +0 -321
  342. package/dist/tracking/stash-manager.js +0 -258
  343. package/dist/tracking/timeline-fork.js +0 -213
  344. package/dist/tracking/timeline.js +0 -69
  345. package/dist/tracking/token-flow.js +0 -276
  346. package/dist/tracking/turn-segmenter.js +0 -122
  347. package/dist/tracking/weekly-accumulator.js +0 -179
  348. package/dist/tracking/working-snapshots.js +0 -188
  349. package/dist/tracking/workspace-manifest.js +0 -176
  350. package/dist/transport/http.js +0 -102
  351. package/dist/utils/counterfactual.js +0 -65
  352. package/dist/utils/deep-link.js +0 -34
  353. package/dist/utils/detect.js +0 -193
  354. package/dist/utils/exec.js +0 -73
  355. package/dist/utils/file-logger.js +0 -87
  356. package/dist/utils/format-error.js +0 -29
  357. package/dist/utils/git.js +0 -181
  358. package/dist/utils/log.js +0 -57
  359. package/dist/utils/logger.js +0 -35
  360. package/dist/utils/mcp-content-json.js +0 -8
  361. package/dist/utils/session-logger.js +0 -154
  362. package/dist/utils/startup-log.js +0 -512
  363. package/dist/utils/ui.js +0 -56
@@ -1,2615 +0,0 @@
1
- /**
2
- * Lightweight AST entity extractor — regex-based, zero-dependency.
3
- *
4
- * Extracts function, class, method, and interface declarations from source files
5
- * using language-specific regex patterns. Designed for drift detection where we
6
- * need entity boundaries (name + line range) without full semantic analysis.
7
- *
8
- * The SCIP indexer handles the heavy lifting for the base graph.
9
- * This extractor only needs to identify "what entities exist in this file" for
10
- * overlay diffing against the CozoDB base entities.
11
- *
12
- * Supported: TypeScript/JavaScript, Python, Go, Java, Rust, C/C++
13
- */
14
- import { createHash } from "node:crypto";
15
- import { existsSync } from "node:fs";
16
- import { join } from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- const EXTENSION_MAP = {
19
- ".ts": "typescript",
20
- ".tsx": "typescript",
21
- ".js": "javascript",
22
- ".jsx": "javascript",
23
- ".mjs": "javascript",
24
- ".cjs": "javascript",
25
- ".py": "python",
26
- ".go": "go",
27
- ".java": "java",
28
- ".rs": "rust",
29
- ".c": "c",
30
- ".h": "c",
31
- ".cpp": "cpp",
32
- ".cc": "cpp",
33
- ".cxx": "cpp",
34
- ".hpp": "cpp",
35
- ".cs": "csharp",
36
- ".rb": "ruby",
37
- ".rake": "ruby",
38
- ".php": "php",
39
- ".kt": "kotlin",
40
- ".kts": "kotlin",
41
- ".swift": "swift",
42
- };
43
- /**
44
- * Detect language from file extension. Returns null for unsupported languages.
45
- */
46
- export function detectLanguage(filePath) {
47
- const dot = filePath.lastIndexOf(".");
48
- if (dot < 0)
49
- return null;
50
- const ext = filePath.slice(dot).toLowerCase();
51
- return EXTENSION_MAP[ext] ?? null;
52
- }
53
- /**
54
- * Extract entities from source code using regex patterns.
55
- * Returns empty array for unsupported languages (never throws).
56
- */
57
- export function extractEntities(content, filePath) {
58
- const language = detectLanguage(filePath);
59
- if (!language)
60
- return [];
61
- const lines = content.split("\n");
62
- const entities = [];
63
- const patterns = getPatterns(language);
64
- let currentClassName = null;
65
- for (let i = 0; i < lines.length; i++) {
66
- const line = lines[i] ?? "";
67
- for (const pattern of patterns) {
68
- const match = pattern.regex.exec(line);
69
- if (match) {
70
- const rawName = match[pattern.nameGroup] ?? "";
71
- if (!rawName || rawName.length === 0)
72
- continue;
73
- // Track current class context for method naming
74
- if (pattern.kind === "class") {
75
- currentClassName = rawName;
76
- }
77
- // Prefix method names with parent class (matches tree-sitter behavior)
78
- const name = pattern.kind === "method" && currentClassName
79
- ? `${currentClassName}.${rawName}`
80
- : rawName;
81
- const signature = match[pattern.sigGroup ?? 0] ?? "";
82
- const lineStart = i + 1; // 1-based
83
- const lineEnd = findBlockEnd(lines, i, language);
84
- const bodyLines = lines.slice(i, lineEnd);
85
- const body = bodyLines.join("\n");
86
- const contentHash = createHash("sha256")
87
- .update(body)
88
- .digest("hex")
89
- .slice(0, 16);
90
- entities.push({
91
- name,
92
- kind: pattern.kind,
93
- signature: signature.trim(),
94
- line_start: lineStart,
95
- line_end: lineEnd,
96
- content_hash: contentHash,
97
- parent_class: pattern.kind === "method"
98
- ? (currentClassName ?? undefined)
99
- : undefined,
100
- });
101
- break; // Only match first pattern per line
102
- }
103
- }
104
- }
105
- return entities;
106
- }
107
- function getPatterns(language) {
108
- switch (language) {
109
- case "typescript":
110
- case "javascript":
111
- return [
112
- // export function name(params) / async function name(params)
113
- {
114
- regex: /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*(\([^)]*\))/,
115
- kind: "function",
116
- nameGroup: 1,
117
- sigGroup: 2,
118
- },
119
- // export class Name / class Name
120
- {
121
- regex: /^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/,
122
- kind: "class",
123
- nameGroup: 1,
124
- },
125
- // export interface Name
126
- {
127
- regex: /^(?:export\s+)?interface\s+(\w+)/,
128
- kind: "interface",
129
- nameGroup: 1,
130
- },
131
- // method(params) { — inside class (with optional access modifiers)
132
- {
133
- regex: /^\s+(?:(?:private|protected|public|override|abstract|readonly)\s+)*(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?(\w+)\s*(\([^)]*\))\s*(?::\s*\S+\s*)?[{]/,
134
- kind: "method",
135
- nameGroup: 1,
136
- sigGroup: 2,
137
- },
138
- // const name = function / const name = (params) =>
139
- {
140
- regex: /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>)/,
141
- kind: "function",
142
- nameGroup: 1,
143
- },
144
- ];
145
- case "python":
146
- return [
147
- // def name(params):
148
- {
149
- regex: /^(?:\s*)(?:async\s+)?def\s+(\w+)\s*(\([^)]*\))/,
150
- kind: "function",
151
- nameGroup: 1,
152
- sigGroup: 2,
153
- },
154
- // class Name:
155
- { regex: /^class\s+(\w+)/, kind: "class", nameGroup: 1 },
156
- ];
157
- case "go":
158
- return [
159
- // func name(params)
160
- {
161
- regex: /^func\s+(\w+)\s*(\([^)]*\))/,
162
- kind: "function",
163
- nameGroup: 1,
164
- sigGroup: 2,
165
- },
166
- // func (r *Type) Name(params)
167
- {
168
- regex: /^func\s+\([^)]+\)\s+(\w+)\s*(\([^)]*\))/,
169
- kind: "method",
170
- nameGroup: 1,
171
- sigGroup: 2,
172
- },
173
- // type Name struct
174
- { regex: /^type\s+(\w+)\s+struct/, kind: "class", nameGroup: 1 },
175
- // type Name interface
176
- { regex: /^type\s+(\w+)\s+interface/, kind: "interface", nameGroup: 1 },
177
- ];
178
- case "java":
179
- return [
180
- // public class Name
181
- {
182
- regex: /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:abstract\s+)?class\s+(\w+)/,
183
- kind: "class",
184
- nameGroup: 1,
185
- },
186
- // public interface Name
187
- {
188
- regex: /^\s*(?:public\s+|private\s+|protected\s+)?interface\s+(\w+)/,
189
- kind: "interface",
190
- nameGroup: 1,
191
- },
192
- // public void name(params)
193
- {
194
- regex: /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:\w+(?:<[^>]+>)?)\s+(\w+)\s*(\([^)]*\))/,
195
- kind: "method",
196
- nameGroup: 1,
197
- sigGroup: 2,
198
- },
199
- ];
200
- case "rust":
201
- return [
202
- // fn name(params)
203
- {
204
- regex: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)\s*(\([^)]*\))/,
205
- kind: "function",
206
- nameGroup: 1,
207
- sigGroup: 2,
208
- },
209
- // struct Name
210
- { regex: /^\s*(?:pub\s+)?struct\s+(\w+)/, kind: "class", nameGroup: 1 },
211
- // trait Name
212
- {
213
- regex: /^\s*(?:pub\s+)?trait\s+(\w+)/,
214
- kind: "interface",
215
- nameGroup: 1,
216
- },
217
- // impl Name
218
- { regex: /^\s*impl(?:<[^>]+>)?\s+(\w+)/, kind: "class", nameGroup: 1 },
219
- ];
220
- case "c":
221
- case "cpp":
222
- return [
223
- // returnType name(params) {
224
- {
225
- regex: /^(?:\w+(?:\s*\*)?)\s+(\w+)\s*(\([^)]*\))\s*\{?$/,
226
- kind: "function",
227
- nameGroup: 1,
228
- sigGroup: 2,
229
- },
230
- // class Name
231
- { regex: /^\s*class\s+(\w+)/, kind: "class", nameGroup: 1 },
232
- // struct Name
233
- {
234
- regex: /^\s*(?:typedef\s+)?struct\s+(\w+)/,
235
- kind: "class",
236
- nameGroup: 1,
237
- },
238
- ];
239
- case "csharp":
240
- return [
241
- // class Name / public class Name
242
- {
243
- regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?(?:abstract\s+|sealed\s+)?(?:partial\s+)?class\s+(\w+)/,
244
- kind: "class",
245
- nameGroup: 1,
246
- },
247
- // interface Name
248
- {
249
- regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:partial\s+)?interface\s+(\w+)/,
250
- kind: "interface",
251
- nameGroup: 1,
252
- },
253
- // struct Name
254
- {
255
- regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:readonly\s+)?(?:partial\s+)?struct\s+(\w+)/,
256
- kind: "class",
257
- nameGroup: 1,
258
- },
259
- // method: returnType Name(params)
260
- {
261
- regex: /^\s+(?:public\s+|private\s+|protected\s+|internal\s+)?(?:static\s+)?(?:async\s+)?(?:virtual\s+|override\s+|abstract\s+)?(?:\w+(?:<[^>]+>)?(?:\[\])?)\s+(\w+)\s*(\([^)]*\))/,
262
- kind: "method",
263
- nameGroup: 1,
264
- sigGroup: 2,
265
- },
266
- ];
267
- case "ruby":
268
- return [
269
- // def name(params) / def self.name(params)
270
- {
271
- regex: /^\s*def\s+(?:self\.)?(\w+[?!=]?)\s*(\([^)]*\))?/,
272
- kind: "function",
273
- nameGroup: 1,
274
- sigGroup: 2,
275
- },
276
- // class Name
277
- { regex: /^\s*class\s+(\w+)/, kind: "class", nameGroup: 1 },
278
- // module Name
279
- { regex: /^\s*module\s+(\w+)/, kind: "interface", nameGroup: 1 },
280
- ];
281
- case "php":
282
- return [
283
- // class Name
284
- {
285
- regex: /^\s*(?:abstract\s+|final\s+)?class\s+(\w+)/,
286
- kind: "class",
287
- nameGroup: 1,
288
- },
289
- // interface Name
290
- {
291
- regex: /^\s*interface\s+(\w+)/,
292
- kind: "interface",
293
- nameGroup: 1,
294
- },
295
- // trait Name
296
- {
297
- regex: /^\s*trait\s+(\w+)/,
298
- kind: "interface",
299
- nameGroup: 1,
300
- },
301
- // function name(params) / public function name(params)
302
- {
303
- regex: /^\s*(?:public\s+|private\s+|protected\s+)?(?:static\s+)?function\s+(\w+)\s*(\([^)]*\))/,
304
- kind: "function",
305
- nameGroup: 1,
306
- sigGroup: 2,
307
- },
308
- ];
309
- case "kotlin":
310
- return [
311
- // class Name / data class Name
312
- {
313
- regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:open\s+|abstract\s+|sealed\s+|data\s+|enum\s+)?class\s+(\w+)/,
314
- kind: "class",
315
- nameGroup: 1,
316
- },
317
- // interface Name
318
- {
319
- regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:sealed\s+)?interface\s+(\w+)/,
320
- kind: "interface",
321
- nameGroup: 1,
322
- },
323
- // object Name
324
- {
325
- regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?object\s+(\w+)/,
326
- kind: "class",
327
- nameGroup: 1,
328
- },
329
- // fun name(params)
330
- {
331
- regex: /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:open\s+|override\s+|abstract\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(\([^)]*\))/,
332
- kind: "function",
333
- nameGroup: 1,
334
- sigGroup: 2,
335
- },
336
- ];
337
- case "swift":
338
- return [
339
- // class Name
340
- {
341
- regex: /^\s*(?:public\s+|private\s+|internal\s+|open\s+|fileprivate\s+)?(?:final\s+)?class\s+(\w+)/,
342
- kind: "class",
343
- nameGroup: 1,
344
- },
345
- // struct Name
346
- {
347
- regex: /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+)?struct\s+(\w+)/,
348
- kind: "class",
349
- nameGroup: 1,
350
- },
351
- // protocol Name
352
- {
353
- regex: /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+)?protocol\s+(\w+)/,
354
- kind: "interface",
355
- nameGroup: 1,
356
- },
357
- // enum Name
358
- {
359
- regex: /^\s*(?:public\s+|private\s+|internal\s+|fileprivate\s+)?enum\s+(\w+)/,
360
- kind: "class",
361
- nameGroup: 1,
362
- },
363
- // func name(params)
364
- {
365
- regex: /^\s*(?:public\s+|private\s+|internal\s+|open\s+|fileprivate\s+)?(?:static\s+|class\s+)?(?:override\s+)?func\s+(\w+)\s*(\([^)]*\))/,
366
- kind: "function",
367
- nameGroup: 1,
368
- sigGroup: 2,
369
- },
370
- ];
371
- default:
372
- return [];
373
- }
374
- }
375
- /**
376
- * Find the end line of a code block starting at the given line.
377
- * Uses brace matching for C-like languages, indentation for Python.
378
- */
379
- function findBlockEnd(lines, startIdx, language) {
380
- if (language === "python") {
381
- return findPythonBlockEnd(lines, startIdx);
382
- }
383
- if (language === "ruby") {
384
- return findRubyBlockEnd(lines, startIdx);
385
- }
386
- return findBraceBlockEnd(lines, startIdx);
387
- }
388
- function findBraceBlockEnd(lines, startIdx) {
389
- let depth = 0;
390
- let foundOpen = false;
391
- for (let i = startIdx; i < lines.length; i++) {
392
- const line = lines[i] ?? "";
393
- for (const ch of line) {
394
- if (ch === "{") {
395
- depth++;
396
- foundOpen = true;
397
- }
398
- else if (ch === "}") {
399
- depth--;
400
- if (foundOpen && depth === 0) {
401
- return i + 1; // 1-based
402
- }
403
- }
404
- }
405
- // If no braces found after 200 lines, cap it
406
- if (i - startIdx > 200)
407
- return i + 1;
408
- }
409
- // If no closing brace, return a reasonable range
410
- return Math.min(startIdx + 10, lines.length);
411
- }
412
- function findPythonBlockEnd(lines, startIdx) {
413
- const startLine = lines[startIdx] ?? "";
414
- const baseIndent = startLine.search(/\S/);
415
- for (let i = startIdx + 1; i < lines.length; i++) {
416
- const line = lines[i] ?? "";
417
- // Skip blank lines
418
- if (line.trim().length === 0)
419
- continue;
420
- const indent = line.search(/\S/);
421
- if (indent <= baseIndent) {
422
- return i; // Previous line was last in block
423
- }
424
- if (i - startIdx > 200)
425
- return i + 1;
426
- }
427
- return lines.length;
428
- }
429
- function findRubyBlockEnd(lines, startIdx) {
430
- const keywords = /^\s*(?:def|class|module|if|unless|while|until|for|begin|case|do)\b/;
431
- let depth = 1; // The opening keyword is on startIdx
432
- for (let i = startIdx + 1; i < lines.length; i++) {
433
- const line = (lines[i] ?? "").trim();
434
- if (line.length === 0 || line.startsWith("#"))
435
- continue;
436
- if (keywords.test(lines[i] ?? ""))
437
- depth++;
438
- if (/^\s*end\b/.test(lines[i] ?? "")) {
439
- depth--;
440
- if (depth === 0)
441
- return i + 1;
442
- }
443
- if (i - startIdx > 200)
444
- return i + 1;
445
- }
446
- return Math.min(startIdx + 10, lines.length);
447
- }
448
- /**
449
- * Generate an entity key that matches the entityHash algorithm.
450
- * Hash inputs: repoId + filePath + kind + name + signature
451
- * Output: 16-char hex (matches lib/indexer/entity-hash.ts)
452
- */
453
- export function entityKey(repoId, filePath, kind, name, signature) {
454
- const input = [repoId, filePath, kind, name, signature ?? ""].join("\0");
455
- return createHash("sha256").update(input).digest("hex").slice(0, 16);
456
- }
457
- /** Tree-sitter grammar name for each language (maps to WASM file). */
458
- const TS_GRAMMAR_MAP = {
459
- typescript: "typescript",
460
- javascript: "javascript",
461
- python: "python",
462
- go: "go",
463
- java: "java",
464
- rust: "rust",
465
- c: "c",
466
- cpp: "cpp",
467
- csharp: "c_sharp",
468
- ruby: "ruby",
469
- php: "php",
470
- kotlin: "kotlin",
471
- swift: "swift",
472
- };
473
- /** TSX file extensions need the tsx grammar, not typescript. */
474
- const TSX_EXTENSIONS = new Set([".tsx", ".jsx"]);
475
- /** Cached parsers keyed by grammar name. null = load attempted but failed. */
476
- const tsParserCache = new Map();
477
- /** Whether tree-sitter init has been called. */
478
- let tsInitDone = false;
479
- /**
480
- * Resolve the grammar name for a file, handling TSX/JSX → tsx grammar.
481
- */
482
- function resolveGrammar(filePath, language) {
483
- const dot = filePath.lastIndexOf(".");
484
- const ext = dot >= 0 ? filePath.slice(dot).toLowerCase() : "";
485
- if (TSX_EXTENSIONS.has(ext))
486
- return "tsx";
487
- return TS_GRAMMAR_MAP[language];
488
- }
489
- /**
490
- * Lazy-load a tree-sitter parser for a given grammar.
491
- * Returns null if WASM not available (graceful degradation to regex).
492
- */
493
- async function getTSParser(grammar) {
494
- if (tsParserCache.has(grammar)) {
495
- return tsParserCache.get(grammar) ?? null;
496
- }
497
- try {
498
- const TreeSitter = (await import("web-tree-sitter")).default;
499
- if (!tsInitDone) {
500
- await TreeSitter.init();
501
- tsInitDone = true;
502
- }
503
- const parser = new TreeSitter();
504
- const wasmFile = `tree-sitter-${grammar}.wasm`;
505
- // Check tree-sitter-wasms/out/ first, then fallback paths.
506
- // Must check BOTH process.cwd() (user project) AND the unerr package dir,
507
- // since unerr often runs in a different project's directory.
508
- const pkgDir = import.meta.dirname ?? join(fileURLToPath(import.meta.url), "..");
509
- const pkgRoot = join(pkgDir, ".."); // dist/ → package root
510
- const possiblePaths = [
511
- join(process.cwd(), "node_modules", "tree-sitter-wasms", "out", wasmFile),
512
- join(process.cwd(), "node_modules", `tree-sitter-${grammar}`, wasmFile),
513
- join(process.cwd(), "node_modules", "web-tree-sitter", wasmFile),
514
- // Package-relative paths (when unerr is installed globally or in another project)
515
- join(pkgRoot, "node_modules", "tree-sitter-wasms", "out", wasmFile),
516
- join(pkgRoot, "node_modules", `tree-sitter-${grammar}`, wasmFile),
517
- join(pkgRoot, "node_modules", "web-tree-sitter", wasmFile),
518
- ];
519
- let wasmPath = null;
520
- for (const p of possiblePaths) {
521
- if (existsSync(p)) {
522
- wasmPath = p;
523
- break;
524
- }
525
- }
526
- if (!wasmPath) {
527
- tsParserCache.set(grammar, null);
528
- return null;
529
- }
530
- const lang = await TreeSitter.Language.load(wasmPath);
531
- parser.setLanguage(lang);
532
- const tsParser = parser;
533
- tsParserCache.set(grammar, tsParser);
534
- return tsParser;
535
- }
536
- catch {
537
- tsParserCache.set(grammar, null);
538
- return null;
539
- }
540
- }
541
- /**
542
- * Pre-load tree-sitter WASM grammars for given languages (DEFERRED startup).
543
- * Called during deferred init so it doesn't block MCP startup.
544
- */
545
- export async function preloadGrammars(languages = [
546
- "typescript",
547
- "javascript",
548
- "python",
549
- "go",
550
- "java",
551
- "rust",
552
- ]) {
553
- const grammars = new Set();
554
- for (const lang of languages) {
555
- grammars.add(TS_GRAMMAR_MAP[lang]);
556
- }
557
- // Also preload tsx
558
- grammars.add("tsx");
559
- await Promise.allSettled([...grammars].map((g) => getTSParser(g)));
560
- }
561
- /**
562
- * Extract entities from source code using tree-sitter AST parsing.
563
- * Falls back to regex-based extraction if WASM is unavailable.
564
- *
565
- * Advantages over regex:
566
- * - No false positives from comments or string literals
567
- * - Methods include parent class context
568
- * - Handles multi-line signatures and nested generics correctly
569
- */
570
- export async function extractEntitiesAsync(content, filePath) {
571
- const language = detectLanguage(filePath);
572
- if (!language)
573
- return [];
574
- const grammar = resolveGrammar(filePath, language);
575
- const parser = await getTSParser(grammar);
576
- if (!parser) {
577
- // Graceful fallback to regex extraction
578
- return extractEntities(content, filePath);
579
- }
580
- try {
581
- const tree = parser.parse(content);
582
- const lines = content.split("\n");
583
- return extractFromAST(tree.rootNode, lines, language);
584
- }
585
- catch {
586
- // Parse failure — fall back to regex
587
- return extractEntities(content, filePath);
588
- }
589
- }
590
- /**
591
- * Walk tree-sitter AST and extract entities based on language-specific node types.
592
- */
593
- function extractFromAST(root, lines, language) {
594
- const entities = [];
595
- switch (language) {
596
- case "typescript":
597
- case "javascript":
598
- extractTSEntities(root, lines, entities);
599
- break;
600
- case "python":
601
- extractPythonEntities(root, lines, entities);
602
- break;
603
- case "go":
604
- extractGoEntities(root, lines, entities);
605
- break;
606
- case "java":
607
- extractJavaEntities(root, lines, entities);
608
- break;
609
- case "rust":
610
- extractRustEntities(root, lines, entities);
611
- break;
612
- case "c":
613
- case "cpp":
614
- extractCEntities(root, lines, entities);
615
- break;
616
- case "csharp":
617
- extractCSharpEntities(root, lines, entities);
618
- break;
619
- case "ruby":
620
- extractRubyEntities(root, lines, entities);
621
- break;
622
- case "php":
623
- extractPHPEntities(root, lines, entities);
624
- break;
625
- case "kotlin":
626
- extractKotlinEntities(root, lines, entities);
627
- break;
628
- case "swift":
629
- extractSwiftEntities(root, lines, entities);
630
- break;
631
- }
632
- return entities;
633
- }
634
- /** Compute content hash from line range. */
635
- function hashLines(lines, startLine, endLine) {
636
- const body = lines.slice(startLine - 1, endLine).join("\n");
637
- return createHash("sha256").update(body).digest("hex").slice(0, 16);
638
- }
639
- /** Get signature text from a parameters/formal_parameters node. */
640
- function getSignature(node, fieldName) {
641
- const params = node.childForFieldName(fieldName);
642
- return params ? params.text : "";
643
- }
644
- /** Find the enclosing class name for a method node. */
645
- function findParentClassName(node) {
646
- let current = node.parent;
647
- while (current) {
648
- if (current.type === "class_declaration" ||
649
- current.type === "class_definition" ||
650
- current.type === "class_body") {
651
- if (current.type === "class_body" && current.parent) {
652
- current = current.parent;
653
- continue;
654
- }
655
- const nameNode = current.childForFieldName("name");
656
- return nameNode ? nameNode.text : null;
657
- }
658
- current = current.parent;
659
- }
660
- return null;
661
- }
662
- // ── TypeScript/JavaScript extraction ────────────────────────────
663
- /**
664
- * Test framework primitives recognized for is_test classification. The first
665
- * dotted segment is matched so `it.only`, `describe.skip`, `test.each(...)`,
666
- * `bench.only` all qualify. When a call_expression's callee matches, the
667
- * function-expression arg (the callback body) is extracted as an entity with
668
- * is_test=true. Top-level helpers in test files (seedEntities, MockEntity)
669
- * stay is_test=false and stop seeding spurious `tests` edges.
670
- */
671
- const TEST_PRIMITIVES = new Set([
672
- "describe",
673
- "it",
674
- "test",
675
- "suite",
676
- "bench",
677
- "context",
678
- "beforeAll",
679
- "beforeEach",
680
- "afterAll",
681
- "afterEach",
682
- "before",
683
- "after",
684
- "fdescribe",
685
- "fit",
686
- "xdescribe",
687
- "xit",
688
- "xtest",
689
- ]);
690
- function extractTSEntities(node, lines, entities) {
691
- walkNodes(node, (n) => {
692
- switch (n.type) {
693
- case "call_expression": {
694
- // Test-primitive callbacks: it("desc", () => {...}) becomes an entity
695
- // with is_test=true so resolveTestEdges can wire it to the source
696
- // entities its calls reach. Pure helpers and fixtures (not inside a
697
- // test primitive) are NOT marked — that's the whole point of this fix.
698
- const fnNode = n.childForFieldName("function");
699
- if (!fnNode)
700
- break;
701
- // First dotted segment — handles `it.only`, `describe.skip`, etc.
702
- const primitive = fnNode.text.split(".")[0]?.trim();
703
- if (!primitive || !TEST_PRIMITIVES.has(primitive))
704
- break;
705
- const argsNode = n.childForFieldName("arguments");
706
- if (!argsNode)
707
- break;
708
- let description = null;
709
- let callback = null;
710
- for (const arg of argsNode.namedChildren) {
711
- if (description === null &&
712
- (arg.type === "string" || arg.type === "template_string")) {
713
- description = arg.text.replace(/^["'`]|["'`]$/g, "");
714
- }
715
- else if (callback === null &&
716
- (arg.type === "arrow_function" ||
717
- arg.type === "function_expression" ||
718
- arg.type === "function")) {
719
- callback = arg;
720
- }
721
- }
722
- if (!callback)
723
- break;
724
- const startLine = callback.startPosition.row + 1;
725
- const endLine = callback.endPosition.row + 1;
726
- const name = description
727
- ? `${primitive}: ${description}`
728
- : `${primitive}@L${startLine}`;
729
- entities.push({
730
- name,
731
- kind: "function",
732
- signature: "",
733
- line_start: startLine,
734
- line_end: endLine,
735
- content_hash: hashLines(lines, startLine, endLine),
736
- is_test: true,
737
- });
738
- break;
739
- }
740
- case "function_declaration": {
741
- const name = n.childForFieldName("name");
742
- if (!name)
743
- return;
744
- const startLine = n.startPosition.row + 1;
745
- const endLine = n.endPosition.row + 1;
746
- entities.push({
747
- name: name.text,
748
- kind: "function",
749
- signature: getSignature(n, "parameters"),
750
- line_start: startLine,
751
- line_end: endLine,
752
- content_hash: hashLines(lines, startLine, endLine),
753
- });
754
- break;
755
- }
756
- case "class_declaration": {
757
- const name = n.childForFieldName("name");
758
- if (!name)
759
- return;
760
- const startLine = n.startPosition.row + 1;
761
- const endLine = n.endPosition.row + 1;
762
- entities.push({
763
- name: name.text,
764
- kind: "class",
765
- signature: "",
766
- line_start: startLine,
767
- line_end: endLine,
768
- content_hash: hashLines(lines, startLine, endLine),
769
- });
770
- break;
771
- }
772
- case "interface_declaration": {
773
- const name = n.childForFieldName("name");
774
- if (!name)
775
- return;
776
- const startLine = n.startPosition.row + 1;
777
- const endLine = n.endPosition.row + 1;
778
- entities.push({
779
- name: name.text,
780
- kind: "interface",
781
- signature: "",
782
- line_start: startLine,
783
- line_end: endLine,
784
- content_hash: hashLines(lines, startLine, endLine),
785
- });
786
- break;
787
- }
788
- case "method_definition": {
789
- const name = n.childForFieldName("name");
790
- if (!name)
791
- return;
792
- const className = findParentClassName(n);
793
- const entityName = className ? `${className}.${name.text}` : name.text;
794
- const startLine = n.startPosition.row + 1;
795
- const endLine = n.endPosition.row + 1;
796
- entities.push({
797
- name: entityName,
798
- kind: "method",
799
- signature: getSignature(n, "parameters"),
800
- line_start: startLine,
801
- line_end: endLine,
802
- content_hash: hashLines(lines, startLine, endLine),
803
- parent_class: className ?? undefined,
804
- });
805
- break;
806
- }
807
- case "lexical_declaration": {
808
- // const foo = (...) => { ... } or const foo = function(...) { ... }
809
- // Also: const FOO = { ... }, const BAR = [...], const X = <expr>
810
- for (const child of n.namedChildren) {
811
- if (child.type === "variable_declarator") {
812
- const nameNode = child.childForFieldName("name");
813
- const valueNode = child.childForFieldName("value");
814
- if (!nameNode || !valueNode)
815
- continue;
816
- const startLine = n.startPosition.row + 1;
817
- const endLine = n.endPosition.row + 1;
818
- if (valueNode.type === "arrow_function" ||
819
- valueNode.type === "function_expression" ||
820
- valueNode.type === "function") {
821
- entities.push({
822
- name: nameNode.text,
823
- kind: "function",
824
- signature: getSignature(valueNode, "parameters"),
825
- line_start: startLine,
826
- line_end: endLine,
827
- content_hash: hashLines(lines, startLine, endLine),
828
- });
829
- }
830
- else if (valueNode.type === "object" ||
831
- valueNode.type === "array" ||
832
- valueNode.type === "as_expression" ||
833
- valueNode.type === "satisfies_expression" ||
834
- valueNode.type === "new_expression" ||
835
- valueNode.type === "call_expression" ||
836
- valueNode.type === "template_string" ||
837
- valueNode.type === "string" ||
838
- valueNode.type === "number" ||
839
- valueNode.type === "true" ||
840
- valueNode.type === "false" ||
841
- valueNode.type === "regex") {
842
- // Only extract if top-level (not nested inside a function/class body)
843
- const isTopLevel = !n.parent ||
844
- n.parent.type === "program" ||
845
- n.parent.type === "export_statement";
846
- if (isTopLevel) {
847
- entities.push({
848
- name: nameNode.text,
849
- kind: "variable",
850
- signature: "",
851
- line_start: startLine,
852
- line_end: endLine,
853
- content_hash: hashLines(lines, startLine, endLine),
854
- });
855
- }
856
- }
857
- }
858
- }
859
- break;
860
- }
861
- case "type_alias_declaration": {
862
- const name = n.childForFieldName("name");
863
- if (!name)
864
- return;
865
- const startLine = n.startPosition.row + 1;
866
- const endLine = n.endPosition.row + 1;
867
- entities.push({
868
- name: name.text,
869
- kind: "interface",
870
- signature: "",
871
- line_start: startLine,
872
- line_end: endLine,
873
- content_hash: hashLines(lines, startLine, endLine),
874
- });
875
- break;
876
- }
877
- case "enum_declaration": {
878
- const name = n.childForFieldName("name");
879
- if (!name)
880
- return;
881
- const startLine = n.startPosition.row + 1;
882
- const endLine = n.endPosition.row + 1;
883
- entities.push({
884
- name: name.text,
885
- kind: "class",
886
- signature: "",
887
- line_start: startLine,
888
- line_end: endLine,
889
- content_hash: hashLines(lines, startLine, endLine),
890
- });
891
- break;
892
- }
893
- }
894
- });
895
- }
896
- // ── Python extraction ───────────────────────────────────────────
897
- function extractPythonEntities(node, lines, entities) {
898
- walkNodes(node, (n) => {
899
- switch (n.type) {
900
- case "function_definition": {
901
- const name = n.childForFieldName("name");
902
- if (!name)
903
- return;
904
- const className = findPythonParentClass(n);
905
- const startLine = n.startPosition.row + 1;
906
- const endLine = n.endPosition.row + 1;
907
- if (className) {
908
- entities.push({
909
- name: `${className}.${name.text}`,
910
- kind: "method",
911
- signature: getSignature(n, "parameters"),
912
- line_start: startLine,
913
- line_end: endLine,
914
- content_hash: hashLines(lines, startLine, endLine),
915
- parent_class: className,
916
- });
917
- }
918
- else {
919
- entities.push({
920
- name: name.text,
921
- kind: "function",
922
- signature: getSignature(n, "parameters"),
923
- line_start: startLine,
924
- line_end: endLine,
925
- content_hash: hashLines(lines, startLine, endLine),
926
- });
927
- }
928
- break;
929
- }
930
- case "class_definition": {
931
- const name = n.childForFieldName("name");
932
- if (!name)
933
- return;
934
- const startLine = n.startPosition.row + 1;
935
- const endLine = n.endPosition.row + 1;
936
- entities.push({
937
- name: name.text,
938
- kind: "class",
939
- signature: "",
940
- line_start: startLine,
941
- line_end: endLine,
942
- content_hash: hashLines(lines, startLine, endLine),
943
- });
944
- break;
945
- }
946
- }
947
- });
948
- }
949
- function findPythonParentClass(node) {
950
- let current = node.parent;
951
- while (current) {
952
- if (current.type === "class_definition") {
953
- const nameNode = current.childForFieldName("name");
954
- return nameNode ? nameNode.text : null;
955
- }
956
- current = current.parent;
957
- }
958
- return null;
959
- }
960
- // ── Go extraction ───────────────────────────────────────────────
961
- function extractGoEntities(node, lines, entities) {
962
- walkNodes(node, (n) => {
963
- switch (n.type) {
964
- case "function_declaration": {
965
- const name = n.childForFieldName("name");
966
- if (!name)
967
- return;
968
- const startLine = n.startPosition.row + 1;
969
- const endLine = n.endPosition.row + 1;
970
- entities.push({
971
- name: name.text,
972
- kind: "function",
973
- signature: getSignature(n, "parameters"),
974
- line_start: startLine,
975
- line_end: endLine,
976
- content_hash: hashLines(lines, startLine, endLine),
977
- });
978
- break;
979
- }
980
- case "method_declaration": {
981
- const name = n.childForFieldName("name");
982
- if (!name)
983
- return;
984
- // Go methods: func (r *Type) Name(params)
985
- const receiver = n.childForFieldName("receiver");
986
- let className = null;
987
- if (receiver) {
988
- // Extract type from receiver parameter list
989
- for (const child of receiver.namedChildren) {
990
- const typeNode = child.childForFieldName("type");
991
- if (typeNode) {
992
- className = typeNode.text.replace(/^\*/, "");
993
- break;
994
- }
995
- }
996
- }
997
- const entityName = className ? `${className}.${name.text}` : name.text;
998
- const startLine = n.startPosition.row + 1;
999
- const endLine = n.endPosition.row + 1;
1000
- entities.push({
1001
- name: entityName,
1002
- kind: "method",
1003
- signature: getSignature(n, "parameters"),
1004
- line_start: startLine,
1005
- line_end: endLine,
1006
- content_hash: hashLines(lines, startLine, endLine),
1007
- parent_class: className ?? undefined,
1008
- });
1009
- break;
1010
- }
1011
- case "type_declaration": {
1012
- // type Name struct { ... } or type Name interface { ... }
1013
- for (const spec of n.namedChildren) {
1014
- if (spec.type === "type_spec") {
1015
- const name = spec.childForFieldName("name");
1016
- const typeNode = spec.childForFieldName("type");
1017
- if (!name || !typeNode)
1018
- continue;
1019
- const kind = typeNode.type === "interface_type" ? "interface" : "class";
1020
- const startLine = n.startPosition.row + 1;
1021
- const endLine = n.endPosition.row + 1;
1022
- entities.push({
1023
- name: name.text,
1024
- kind,
1025
- signature: "",
1026
- line_start: startLine,
1027
- line_end: endLine,
1028
- content_hash: hashLines(lines, startLine, endLine),
1029
- });
1030
- }
1031
- }
1032
- break;
1033
- }
1034
- }
1035
- });
1036
- }
1037
- // ── Java extraction ─────────────────────────────────────────────
1038
- function extractJavaEntities(node, lines, entities) {
1039
- walkNodes(node, (n) => {
1040
- switch (n.type) {
1041
- case "class_declaration": {
1042
- const name = n.childForFieldName("name");
1043
- if (!name)
1044
- return;
1045
- const startLine = n.startPosition.row + 1;
1046
- const endLine = n.endPosition.row + 1;
1047
- entities.push({
1048
- name: name.text,
1049
- kind: "class",
1050
- signature: "",
1051
- line_start: startLine,
1052
- line_end: endLine,
1053
- content_hash: hashLines(lines, startLine, endLine),
1054
- });
1055
- break;
1056
- }
1057
- case "interface_declaration": {
1058
- const name = n.childForFieldName("name");
1059
- if (!name)
1060
- return;
1061
- const startLine = n.startPosition.row + 1;
1062
- const endLine = n.endPosition.row + 1;
1063
- entities.push({
1064
- name: name.text,
1065
- kind: "interface",
1066
- signature: "",
1067
- line_start: startLine,
1068
- line_end: endLine,
1069
- content_hash: hashLines(lines, startLine, endLine),
1070
- });
1071
- break;
1072
- }
1073
- case "method_declaration": {
1074
- const name = n.childForFieldName("name");
1075
- if (!name)
1076
- return;
1077
- const className = findJavaParentClass(n);
1078
- const entityName = className ? `${className}.${name.text}` : name.text;
1079
- const startLine = n.startPosition.row + 1;
1080
- const endLine = n.endPosition.row + 1;
1081
- entities.push({
1082
- name: entityName,
1083
- kind: "method",
1084
- signature: getSignature(n, "parameters"),
1085
- line_start: startLine,
1086
- line_end: endLine,
1087
- content_hash: hashLines(lines, startLine, endLine),
1088
- parent_class: className ?? undefined,
1089
- });
1090
- break;
1091
- }
1092
- }
1093
- });
1094
- }
1095
- function findJavaParentClass(node) {
1096
- let current = node.parent;
1097
- while (current) {
1098
- if (current.type === "class_declaration" ||
1099
- current.type === "interface_declaration") {
1100
- const nameNode = current.childForFieldName("name");
1101
- return nameNode ? nameNode.text : null;
1102
- }
1103
- current = current.parent;
1104
- }
1105
- return null;
1106
- }
1107
- // ── Rust extraction ─────────────────────────────────────────────
1108
- /**
1109
- * Check if a node is inside a #[cfg(test)] module by walking up the tree.
1110
- * Detects attribute_item siblings of mod_item ancestors containing "cfg" and "test".
1111
- */
1112
- function isInsideCfgTest(node) {
1113
- let current = node.parent;
1114
- while (current) {
1115
- if (current.type === "mod_item") {
1116
- let sibling = current.previousSibling;
1117
- while (sibling) {
1118
- if (sibling.type === "attribute_item") {
1119
- const text = sibling.text;
1120
- if (text.includes("cfg") && text.includes("test"))
1121
- return true;
1122
- }
1123
- else if (sibling.type !== "line_comment" &&
1124
- sibling.type !== "block_comment") {
1125
- break;
1126
- }
1127
- sibling = sibling.previousSibling;
1128
- }
1129
- }
1130
- current = current.parent;
1131
- }
1132
- return false;
1133
- }
1134
- function extractRustEntities(node, lines, entities) {
1135
- walkNodes(node, (n) => {
1136
- switch (n.type) {
1137
- case "function_item": {
1138
- const name = n.childForFieldName("name");
1139
- if (!name)
1140
- return;
1141
- // Check if inside impl block
1142
- const implName = findRustImplName(n);
1143
- const entityName = implName ? `${implName}.${name.text}` : name.text;
1144
- const kind = implName ? "method" : "function";
1145
- const startLine = n.startPosition.row + 1;
1146
- const endLine = n.endPosition.row + 1;
1147
- const entity = {
1148
- name: entityName,
1149
- kind,
1150
- signature: getSignature(n, "parameters"),
1151
- line_start: startLine,
1152
- line_end: endLine,
1153
- content_hash: hashLines(lines, startLine, endLine),
1154
- parent_class: implName ?? undefined,
1155
- };
1156
- if (isInsideCfgTest(n))
1157
- entity.is_test = true;
1158
- entities.push(entity);
1159
- break;
1160
- }
1161
- case "struct_item": {
1162
- const name = n.childForFieldName("name");
1163
- if (!name)
1164
- return;
1165
- const startLine = n.startPosition.row + 1;
1166
- const endLine = n.endPosition.row + 1;
1167
- const entity = {
1168
- name: name.text,
1169
- kind: "class",
1170
- signature: "",
1171
- line_start: startLine,
1172
- line_end: endLine,
1173
- content_hash: hashLines(lines, startLine, endLine),
1174
- };
1175
- if (isInsideCfgTest(n))
1176
- entity.is_test = true;
1177
- entities.push(entity);
1178
- break;
1179
- }
1180
- case "trait_item": {
1181
- const name = n.childForFieldName("name");
1182
- if (!name)
1183
- return;
1184
- const startLine = n.startPosition.row + 1;
1185
- const endLine = n.endPosition.row + 1;
1186
- const entity = {
1187
- name: name.text,
1188
- kind: "interface",
1189
- signature: "",
1190
- line_start: startLine,
1191
- line_end: endLine,
1192
- content_hash: hashLines(lines, startLine, endLine),
1193
- };
1194
- if (isInsideCfgTest(n))
1195
- entity.is_test = true;
1196
- entities.push(entity);
1197
- break;
1198
- }
1199
- case "impl_item": {
1200
- const name = n.childForFieldName("type");
1201
- if (!name)
1202
- return;
1203
- const startLine = n.startPosition.row + 1;
1204
- const endLine = n.endPosition.row + 1;
1205
- const entity = {
1206
- name: name.text,
1207
- kind: "class",
1208
- signature: "",
1209
- line_start: startLine,
1210
- line_end: endLine,
1211
- content_hash: hashLines(lines, startLine, endLine),
1212
- };
1213
- if (isInsideCfgTest(n))
1214
- entity.is_test = true;
1215
- entities.push(entity);
1216
- break;
1217
- }
1218
- }
1219
- });
1220
- }
1221
- function findRustImplName(node) {
1222
- let current = node.parent;
1223
- while (current) {
1224
- if (current.type === "impl_item") {
1225
- const typeNode = current.childForFieldName("type");
1226
- return typeNode ? typeNode.text : null;
1227
- }
1228
- // Skip declaration_list (impl body)
1229
- current = current.parent;
1230
- }
1231
- return null;
1232
- }
1233
- // ── C/C++ extraction ────────────────────────────────────────────
1234
- function extractCEntities(node, lines, entities) {
1235
- walkNodes(node, (n) => {
1236
- switch (n.type) {
1237
- case "function_definition": {
1238
- const declarator = n.childForFieldName("declarator");
1239
- if (!declarator)
1240
- return;
1241
- // function_declarator has name and parameters
1242
- const name = declarator.type === "function_declarator"
1243
- ? declarator.childForFieldName("declarator")
1244
- : null;
1245
- if (!name)
1246
- return;
1247
- const startLine = n.startPosition.row + 1;
1248
- const endLine = n.endPosition.row + 1;
1249
- entities.push({
1250
- name: name.text,
1251
- kind: "function",
1252
- signature: getSignature(declarator, "parameters"),
1253
- line_start: startLine,
1254
- line_end: endLine,
1255
- content_hash: hashLines(lines, startLine, endLine),
1256
- });
1257
- break;
1258
- }
1259
- case "struct_specifier": {
1260
- const name = n.childForFieldName("name");
1261
- if (!name)
1262
- return;
1263
- const startLine = n.startPosition.row + 1;
1264
- const endLine = n.endPosition.row + 1;
1265
- entities.push({
1266
- name: name.text,
1267
- kind: "class",
1268
- signature: "",
1269
- line_start: startLine,
1270
- line_end: endLine,
1271
- content_hash: hashLines(lines, startLine, endLine),
1272
- });
1273
- break;
1274
- }
1275
- case "class_specifier": {
1276
- const name = n.childForFieldName("name");
1277
- if (!name)
1278
- return;
1279
- const startLine = n.startPosition.row + 1;
1280
- const endLine = n.endPosition.row + 1;
1281
- entities.push({
1282
- name: name.text,
1283
- kind: "class",
1284
- signature: "",
1285
- line_start: startLine,
1286
- line_end: endLine,
1287
- content_hash: hashLines(lines, startLine, endLine),
1288
- });
1289
- break;
1290
- }
1291
- }
1292
- });
1293
- }
1294
- // ── C# extraction ──────────────────────────────────────────────
1295
- function extractCSharpEntities(node, lines, entities) {
1296
- walkNodes(node, (n) => {
1297
- switch (n.type) {
1298
- case "class_declaration": {
1299
- const name = n.childForFieldName("name");
1300
- if (!name)
1301
- return;
1302
- const startLine = n.startPosition.row + 1;
1303
- const endLine = n.endPosition.row + 1;
1304
- entities.push({
1305
- name: name.text,
1306
- kind: "class",
1307
- signature: "",
1308
- line_start: startLine,
1309
- line_end: endLine,
1310
- content_hash: hashLines(lines, startLine, endLine),
1311
- });
1312
- break;
1313
- }
1314
- case "interface_declaration": {
1315
- const name = n.childForFieldName("name");
1316
- if (!name)
1317
- return;
1318
- const startLine = n.startPosition.row + 1;
1319
- const endLine = n.endPosition.row + 1;
1320
- entities.push({
1321
- name: name.text,
1322
- kind: "interface",
1323
- signature: "",
1324
- line_start: startLine,
1325
- line_end: endLine,
1326
- content_hash: hashLines(lines, startLine, endLine),
1327
- });
1328
- break;
1329
- }
1330
- case "struct_declaration": {
1331
- const name = n.childForFieldName("name");
1332
- if (!name)
1333
- return;
1334
- const startLine = n.startPosition.row + 1;
1335
- const endLine = n.endPosition.row + 1;
1336
- entities.push({
1337
- name: name.text,
1338
- kind: "class",
1339
- signature: "",
1340
- line_start: startLine,
1341
- line_end: endLine,
1342
- content_hash: hashLines(lines, startLine, endLine),
1343
- });
1344
- break;
1345
- }
1346
- case "method_declaration": {
1347
- const name = n.childForFieldName("name");
1348
- if (!name)
1349
- return;
1350
- const className = findCSharpParentClass(n);
1351
- const entityName = className ? `${className}.${name.text}` : name.text;
1352
- const startLine = n.startPosition.row + 1;
1353
- const endLine = n.endPosition.row + 1;
1354
- entities.push({
1355
- name: entityName,
1356
- kind: "method",
1357
- signature: getSignature(n, "parameters"),
1358
- line_start: startLine,
1359
- line_end: endLine,
1360
- content_hash: hashLines(lines, startLine, endLine),
1361
- parent_class: className ?? undefined,
1362
- });
1363
- break;
1364
- }
1365
- }
1366
- });
1367
- }
1368
- function findCSharpParentClass(node) {
1369
- let current = node.parent;
1370
- while (current) {
1371
- if (current.type === "class_declaration" ||
1372
- current.type === "struct_declaration" ||
1373
- current.type === "interface_declaration") {
1374
- const nameNode = current.childForFieldName("name");
1375
- return nameNode ? nameNode.text : null;
1376
- }
1377
- current = current.parent;
1378
- }
1379
- return null;
1380
- }
1381
- // ── Ruby extraction ────────────────────────────────────────────
1382
- function extractRubyEntities(node, lines, entities) {
1383
- walkNodes(node, (n) => {
1384
- switch (n.type) {
1385
- case "class": {
1386
- const name = n.childForFieldName("name");
1387
- if (!name)
1388
- return;
1389
- const startLine = n.startPosition.row + 1;
1390
- const endLine = n.endPosition.row + 1;
1391
- entities.push({
1392
- name: name.text,
1393
- kind: "class",
1394
- signature: "",
1395
- line_start: startLine,
1396
- line_end: endLine,
1397
- content_hash: hashLines(lines, startLine, endLine),
1398
- });
1399
- break;
1400
- }
1401
- case "module": {
1402
- const name = n.childForFieldName("name");
1403
- if (!name)
1404
- return;
1405
- const startLine = n.startPosition.row + 1;
1406
- const endLine = n.endPosition.row + 1;
1407
- entities.push({
1408
- name: name.text,
1409
- kind: "interface",
1410
- signature: "",
1411
- line_start: startLine,
1412
- line_end: endLine,
1413
- content_hash: hashLines(lines, startLine, endLine),
1414
- });
1415
- break;
1416
- }
1417
- case "method":
1418
- case "singleton_method": {
1419
- const name = n.childForFieldName("name");
1420
- if (!name)
1421
- return;
1422
- const className = findRubyParentClass(n);
1423
- const entityName = className ? `${className}.${name.text}` : name.text;
1424
- const kind = className ? "method" : "function";
1425
- const startLine = n.startPosition.row + 1;
1426
- const endLine = n.endPosition.row + 1;
1427
- entities.push({
1428
- name: entityName,
1429
- kind,
1430
- signature: getSignature(n, "parameters"),
1431
- line_start: startLine,
1432
- line_end: endLine,
1433
- content_hash: hashLines(lines, startLine, endLine),
1434
- parent_class: className ?? undefined,
1435
- });
1436
- break;
1437
- }
1438
- }
1439
- });
1440
- }
1441
- function findRubyParentClass(node) {
1442
- let current = node.parent;
1443
- while (current) {
1444
- if (current.type === "class") {
1445
- const nameNode = current.childForFieldName("name");
1446
- return nameNode ? nameNode.text : null;
1447
- }
1448
- current = current.parent;
1449
- }
1450
- return null;
1451
- }
1452
- // ── PHP extraction ─────────────────────────────────────────────
1453
- function extractPHPEntities(node, lines, entities) {
1454
- walkNodes(node, (n) => {
1455
- switch (n.type) {
1456
- case "class_declaration": {
1457
- const name = n.childForFieldName("name");
1458
- if (!name)
1459
- return;
1460
- const startLine = n.startPosition.row + 1;
1461
- const endLine = n.endPosition.row + 1;
1462
- entities.push({
1463
- name: name.text,
1464
- kind: "class",
1465
- signature: "",
1466
- line_start: startLine,
1467
- line_end: endLine,
1468
- content_hash: hashLines(lines, startLine, endLine),
1469
- });
1470
- break;
1471
- }
1472
- case "interface_declaration": {
1473
- const name = n.childForFieldName("name");
1474
- if (!name)
1475
- return;
1476
- const startLine = n.startPosition.row + 1;
1477
- const endLine = n.endPosition.row + 1;
1478
- entities.push({
1479
- name: name.text,
1480
- kind: "interface",
1481
- signature: "",
1482
- line_start: startLine,
1483
- line_end: endLine,
1484
- content_hash: hashLines(lines, startLine, endLine),
1485
- });
1486
- break;
1487
- }
1488
- case "trait_declaration": {
1489
- const name = n.childForFieldName("name");
1490
- if (!name)
1491
- return;
1492
- const startLine = n.startPosition.row + 1;
1493
- const endLine = n.endPosition.row + 1;
1494
- entities.push({
1495
- name: name.text,
1496
- kind: "interface",
1497
- signature: "",
1498
- line_start: startLine,
1499
- line_end: endLine,
1500
- content_hash: hashLines(lines, startLine, endLine),
1501
- });
1502
- break;
1503
- }
1504
- case "function_definition": {
1505
- const name = n.childForFieldName("name");
1506
- if (!name)
1507
- return;
1508
- const startLine = n.startPosition.row + 1;
1509
- const endLine = n.endPosition.row + 1;
1510
- entities.push({
1511
- name: name.text,
1512
- kind: "function",
1513
- signature: getSignature(n, "parameters"),
1514
- line_start: startLine,
1515
- line_end: endLine,
1516
- content_hash: hashLines(lines, startLine, endLine),
1517
- });
1518
- break;
1519
- }
1520
- case "method_declaration": {
1521
- const name = n.childForFieldName("name");
1522
- if (!name)
1523
- return;
1524
- const className = findPHPParentClass(n);
1525
- const entityName = className ? `${className}.${name.text}` : name.text;
1526
- const startLine = n.startPosition.row + 1;
1527
- const endLine = n.endPosition.row + 1;
1528
- entities.push({
1529
- name: entityName,
1530
- kind: "method",
1531
- signature: getSignature(n, "parameters"),
1532
- line_start: startLine,
1533
- line_end: endLine,
1534
- content_hash: hashLines(lines, startLine, endLine),
1535
- parent_class: className ?? undefined,
1536
- });
1537
- break;
1538
- }
1539
- }
1540
- });
1541
- }
1542
- function findPHPParentClass(node) {
1543
- let current = node.parent;
1544
- while (current) {
1545
- if (current.type === "class_declaration" ||
1546
- current.type === "trait_declaration" ||
1547
- current.type === "interface_declaration") {
1548
- const nameNode = current.childForFieldName("name");
1549
- return nameNode ? nameNode.text : null;
1550
- }
1551
- current = current.parent;
1552
- }
1553
- return null;
1554
- }
1555
- // ── Kotlin extraction ──────────────────────────────────────────
1556
- function extractKotlinEntities(node, lines, entities) {
1557
- walkNodes(node, (n) => {
1558
- switch (n.type) {
1559
- case "class_declaration": {
1560
- const name = findFirstIdentifier(n);
1561
- if (!name)
1562
- return;
1563
- const startLine = n.startPosition.row + 1;
1564
- const endLine = n.endPosition.row + 1;
1565
- entities.push({
1566
- name,
1567
- kind: "class",
1568
- signature: "",
1569
- line_start: startLine,
1570
- line_end: endLine,
1571
- content_hash: hashLines(lines, startLine, endLine),
1572
- });
1573
- break;
1574
- }
1575
- case "interface_declaration": {
1576
- const name = findFirstIdentifier(n);
1577
- if (!name)
1578
- return;
1579
- const startLine = n.startPosition.row + 1;
1580
- const endLine = n.endPosition.row + 1;
1581
- entities.push({
1582
- name,
1583
- kind: "interface",
1584
- signature: "",
1585
- line_start: startLine,
1586
- line_end: endLine,
1587
- content_hash: hashLines(lines, startLine, endLine),
1588
- });
1589
- break;
1590
- }
1591
- case "object_declaration": {
1592
- const name = findFirstIdentifier(n);
1593
- if (!name)
1594
- return;
1595
- const startLine = n.startPosition.row + 1;
1596
- const endLine = n.endPosition.row + 1;
1597
- entities.push({
1598
- name,
1599
- kind: "class",
1600
- signature: "",
1601
- line_start: startLine,
1602
- line_end: endLine,
1603
- content_hash: hashLines(lines, startLine, endLine),
1604
- });
1605
- break;
1606
- }
1607
- case "function_declaration": {
1608
- const name = (n.childForFieldName("name") ?? findFirstIdentifier(n))
1609
- ? { text: findFirstIdentifier(n) }
1610
- : null;
1611
- if (!name?.text)
1612
- return;
1613
- const className = findKotlinParentClass(n);
1614
- const entityName = className ? `${className}.${name.text}` : name.text;
1615
- const kind = className ? "method" : "function";
1616
- const startLine = n.startPosition.row + 1;
1617
- const endLine = n.endPosition.row + 1;
1618
- entities.push({
1619
- name: entityName,
1620
- kind,
1621
- signature: getSignature(n, "value_parameters"),
1622
- line_start: startLine,
1623
- line_end: endLine,
1624
- content_hash: hashLines(lines, startLine, endLine),
1625
- parent_class: className ?? undefined,
1626
- });
1627
- break;
1628
- }
1629
- }
1630
- });
1631
- }
1632
- /** Find the first simple_identifier child of a node (Kotlin grammars use this instead of "name" field). */
1633
- function findFirstIdentifier(node) {
1634
- for (const child of node.namedChildren) {
1635
- if (child.type === "type_identifier" ||
1636
- child.type === "simple_identifier") {
1637
- return child.text;
1638
- }
1639
- }
1640
- return null;
1641
- }
1642
- function findKotlinParentClass(node) {
1643
- let current = node.parent;
1644
- while (current) {
1645
- if (current.type === "class_declaration" ||
1646
- current.type === "object_declaration" ||
1647
- current.type === "interface_declaration") {
1648
- return findFirstIdentifier(current);
1649
- }
1650
- // Skip class_body
1651
- current = current.parent;
1652
- }
1653
- return null;
1654
- }
1655
- // ── Swift extraction ───────────────────────────────────────────
1656
- function extractSwiftEntities(node, lines, entities) {
1657
- walkNodes(node, (n) => {
1658
- switch (n.type) {
1659
- case "class_declaration": {
1660
- const name = n.childForFieldName("name");
1661
- if (!name)
1662
- return;
1663
- const startLine = n.startPosition.row + 1;
1664
- const endLine = n.endPosition.row + 1;
1665
- entities.push({
1666
- name: name.text,
1667
- kind: "class",
1668
- signature: "",
1669
- line_start: startLine,
1670
- line_end: endLine,
1671
- content_hash: hashLines(lines, startLine, endLine),
1672
- });
1673
- break;
1674
- }
1675
- case "struct_declaration": {
1676
- const name = n.childForFieldName("name");
1677
- if (!name)
1678
- return;
1679
- const startLine = n.startPosition.row + 1;
1680
- const endLine = n.endPosition.row + 1;
1681
- entities.push({
1682
- name: name.text,
1683
- kind: "class",
1684
- signature: "",
1685
- line_start: startLine,
1686
- line_end: endLine,
1687
- content_hash: hashLines(lines, startLine, endLine),
1688
- });
1689
- break;
1690
- }
1691
- case "protocol_declaration": {
1692
- const name = n.childForFieldName("name");
1693
- if (!name)
1694
- return;
1695
- const startLine = n.startPosition.row + 1;
1696
- const endLine = n.endPosition.row + 1;
1697
- entities.push({
1698
- name: name.text,
1699
- kind: "interface",
1700
- signature: "",
1701
- line_start: startLine,
1702
- line_end: endLine,
1703
- content_hash: hashLines(lines, startLine, endLine),
1704
- });
1705
- break;
1706
- }
1707
- case "enum_declaration": {
1708
- const name = n.childForFieldName("name");
1709
- if (!name)
1710
- return;
1711
- const startLine = n.startPosition.row + 1;
1712
- const endLine = n.endPosition.row + 1;
1713
- entities.push({
1714
- name: name.text,
1715
- kind: "class",
1716
- signature: "",
1717
- line_start: startLine,
1718
- line_end: endLine,
1719
- content_hash: hashLines(lines, startLine, endLine),
1720
- });
1721
- break;
1722
- }
1723
- case "function_declaration": {
1724
- const name = n.childForFieldName("name");
1725
- if (!name)
1726
- return;
1727
- const className = findSwiftParentClass(n);
1728
- const entityName = className ? `${className}.${name.text}` : name.text;
1729
- const kind = className ? "method" : "function";
1730
- const startLine = n.startPosition.row + 1;
1731
- const endLine = n.endPosition.row + 1;
1732
- entities.push({
1733
- name: entityName,
1734
- kind,
1735
- signature: getSignature(n, "parameters"),
1736
- line_start: startLine,
1737
- line_end: endLine,
1738
- content_hash: hashLines(lines, startLine, endLine),
1739
- parent_class: className ?? undefined,
1740
- });
1741
- break;
1742
- }
1743
- }
1744
- });
1745
- }
1746
- function findSwiftParentClass(node) {
1747
- let current = node.parent;
1748
- while (current) {
1749
- if (current.type === "class_declaration" ||
1750
- current.type === "struct_declaration" ||
1751
- current.type === "enum_declaration" ||
1752
- current.type === "protocol_declaration") {
1753
- const nameNode = current.childForFieldName("name");
1754
- return nameNode ? nameNode.text : null;
1755
- }
1756
- current = current.parent;
1757
- }
1758
- return null;
1759
- }
1760
- /**
1761
- * Extract edges (imports, calls, extends, implements) from source code using tree-sitter AST.
1762
- * Falls back to regex-based extraction if WASM is unavailable.
1763
- */
1764
- export async function extractEdgesAsync(content, filePath, entities) {
1765
- const language = detectLanguage(filePath);
1766
- if (!language)
1767
- return [];
1768
- const grammar = resolveGrammar(filePath, language);
1769
- const parser = await getTSParser(grammar);
1770
- if (!parser) {
1771
- return extractEdgesRegex(content, filePath, language);
1772
- }
1773
- try {
1774
- const tree = parser.parse(content);
1775
- return extractEdgesFromAST(tree.rootNode, language, entities);
1776
- }
1777
- catch {
1778
- return extractEdgesRegex(content, filePath, language);
1779
- }
1780
- }
1781
- /** Extract edges from tree-sitter AST by language. */
1782
- function extractEdgesFromAST(root, language, entities) {
1783
- const edges = [];
1784
- switch (language) {
1785
- case "typescript":
1786
- case "javascript":
1787
- extractTSEdges(root, edges, entities);
1788
- break;
1789
- case "python":
1790
- extractPythonEdges(root, edges);
1791
- break;
1792
- case "go":
1793
- extractGoEdges(root, edges);
1794
- break;
1795
- case "java":
1796
- extractJavaEdges(root, edges);
1797
- break;
1798
- case "rust":
1799
- extractRustEdges(root, edges);
1800
- break;
1801
- case "csharp":
1802
- extractCSharpEdges(root, edges);
1803
- break;
1804
- case "ruby":
1805
- extractRubyEdges(root, edges);
1806
- break;
1807
- case "php":
1808
- extractPHPEdges(root, edges);
1809
- break;
1810
- case "kotlin":
1811
- extractKotlinEdges(root, edges);
1812
- break;
1813
- case "swift":
1814
- extractSwiftEdges(root, edges);
1815
- break;
1816
- default:
1817
- break;
1818
- }
1819
- return edges;
1820
- }
1821
- /** TS/JS: extract imports, calls, extends, implements from AST. */
1822
- function extractTSEdges(root, edges, entities) {
1823
- const entityNames = new Set(entities.map((e) => e.name.split(".").pop() ?? e.name));
1824
- // Collect imported symbol names so cross-file calls are captured.
1825
- // First pass: gather import names before recording call edges.
1826
- const importedNames = new Set();
1827
- walkNodes(root, (n) => {
1828
- if (n.type === "import_statement") {
1829
- for (const child of n.namedChildren) {
1830
- if (child.type === "import_clause") {
1831
- for (const spec of child.namedChildren) {
1832
- if (spec.type === "named_imports") {
1833
- for (const s of spec.namedChildren) {
1834
- if (s.type === "import_specifier") {
1835
- const name = s.childForFieldName("name");
1836
- if (name)
1837
- importedNames.add(name.text);
1838
- }
1839
- }
1840
- }
1841
- else if (spec.type === "identifier") {
1842
- importedNames.add(spec.text);
1843
- }
1844
- }
1845
- }
1846
- }
1847
- }
1848
- });
1849
- walkNodes(root, (n) => {
1850
- // import { Foo, Bar } from "./module"
1851
- if (n.type === "import_statement") {
1852
- const source = n.childForFieldName("source");
1853
- const importSource = source ? source.text.replace(/['"]/g, "") : "";
1854
- // Named imports
1855
- for (const child of n.namedChildren) {
1856
- if (child.type === "import_clause") {
1857
- for (const spec of child.namedChildren) {
1858
- if (spec.type === "named_imports") {
1859
- for (const s of spec.namedChildren) {
1860
- if (s.type === "import_specifier") {
1861
- const name = s.childForFieldName("name");
1862
- if (name) {
1863
- edges.push({
1864
- from_name: "__file__",
1865
- to_name: name.text,
1866
- type: "imports",
1867
- import_source: importSource,
1868
- });
1869
- }
1870
- }
1871
- }
1872
- }
1873
- else if (spec.type === "identifier") {
1874
- // Default import
1875
- edges.push({
1876
- from_name: "__file__",
1877
- to_name: spec.text,
1878
- type: "imports",
1879
- import_source: importSource,
1880
- });
1881
- }
1882
- }
1883
- }
1884
- }
1885
- }
1886
- // class Foo extends Bar implements Baz
1887
- if (n.type === "class_declaration") {
1888
- const className = n.childForFieldName("name");
1889
- if (!className)
1890
- return;
1891
- // Heritage: extends and implements
1892
- for (const child of n.namedChildren) {
1893
- if (child.type === "class_heritage") {
1894
- for (const clause of child.namedChildren) {
1895
- if (clause.type === "extends_clause") {
1896
- for (const val of clause.namedChildren) {
1897
- if (val.type === "identifier") {
1898
- edges.push({
1899
- from_name: className.text,
1900
- to_name: val.text,
1901
- type: "extends",
1902
- });
1903
- }
1904
- }
1905
- }
1906
- if (clause.type === "implements_clause") {
1907
- for (const val of clause.namedChildren) {
1908
- if (val.type === "type_identifier" ||
1909
- val.type === "identifier") {
1910
- edges.push({
1911
- from_name: className.text,
1912
- to_name: val.text,
1913
- type: "implements",
1914
- });
1915
- }
1916
- }
1917
- }
1918
- }
1919
- }
1920
- }
1921
- }
1922
- // Function calls: foo(), bar.baz(), new Foo()
1923
- if (n.type === "call_expression") {
1924
- const func = n.childForFieldName("function");
1925
- if (!func)
1926
- return;
1927
- let calledName = null;
1928
- if (func.type === "identifier") {
1929
- calledName = func.text;
1930
- }
1931
- else if (func.type === "member_expression") {
1932
- const prop = func.childForFieldName("property");
1933
- if (prop)
1934
- calledName = prop.text;
1935
- }
1936
- if (calledName &&
1937
- (entityNames.has(calledName) || importedNames.has(calledName))) {
1938
- const caller = findEnclosingEntity(n, entities);
1939
- edges.push({
1940
- from_name: caller ?? "__file__",
1941
- to_name: calledName,
1942
- type: "calls",
1943
- });
1944
- }
1945
- }
1946
- // new Foo()
1947
- if (n.type === "new_expression") {
1948
- const ctor = n.childForFieldName("constructor");
1949
- if (ctor && ctor.type === "identifier") {
1950
- const caller = findEnclosingEntity(n, entities);
1951
- edges.push({
1952
- from_name: caller ?? "__file__",
1953
- to_name: ctor.text,
1954
- type: "calls",
1955
- });
1956
- }
1957
- }
1958
- });
1959
- }
1960
- /** Python: extract imports, calls, class inheritance. */
1961
- function extractPythonEdges(root, edges) {
1962
- walkNodes(root, (n) => {
1963
- // import foo / from foo import bar
1964
- if (n.type === "import_statement" || n.type === "import_from_statement") {
1965
- const moduleName = n.childForFieldName("module_name");
1966
- const importSource = moduleName ? moduleName.text : "";
1967
- for (const child of n.namedChildren) {
1968
- if (child.type === "dotted_name" && child !== moduleName) {
1969
- edges.push({
1970
- from_name: "__file__",
1971
- to_name: child.text.split(".").pop() ?? child.text,
1972
- type: "imports",
1973
- import_source: importSource,
1974
- });
1975
- }
1976
- if (child.type === "aliased_import") {
1977
- const name = child.childForFieldName("name");
1978
- if (name) {
1979
- edges.push({
1980
- from_name: "__file__",
1981
- to_name: name.text.split(".").pop() ?? name.text,
1982
- type: "imports",
1983
- import_source: importSource,
1984
- });
1985
- }
1986
- }
1987
- }
1988
- }
1989
- // class Foo(Bar, Baz):
1990
- if (n.type === "class_definition") {
1991
- const name = n.childForFieldName("name");
1992
- const superclasses = n.childForFieldName("superclasses");
1993
- if (name && superclasses) {
1994
- for (const arg of superclasses.namedChildren) {
1995
- if (arg.type === "identifier") {
1996
- edges.push({
1997
- from_name: name.text,
1998
- to_name: arg.text,
1999
- type: "extends",
2000
- });
2001
- }
2002
- }
2003
- }
2004
- }
2005
- });
2006
- }
2007
- /** Go: extract imports and struct embedding. */
2008
- function extractGoEdges(root, edges) {
2009
- walkNodes(root, (n) => {
2010
- if (n.type === "import_declaration") {
2011
- for (const child of n.namedChildren) {
2012
- if (child.type === "import_spec" || child.type === "import_spec_list") {
2013
- const specs = child.type === "import_spec_list" ? child.namedChildren : [child];
2014
- for (const spec of specs) {
2015
- if (spec.type === "import_spec") {
2016
- const path = spec.childForFieldName("path");
2017
- if (path) {
2018
- const importPath = path.text.replace(/"/g, "");
2019
- const pkgName = importPath.split("/").pop() ?? importPath;
2020
- edges.push({
2021
- from_name: "__file__",
2022
- to_name: pkgName,
2023
- type: "imports",
2024
- import_source: importPath,
2025
- });
2026
- }
2027
- }
2028
- }
2029
- }
2030
- }
2031
- }
2032
- });
2033
- }
2034
- /** Java: extract imports and extends/implements. */
2035
- function extractJavaEdges(root, edges) {
2036
- walkNodes(root, (n) => {
2037
- if (n.type === "import_declaration") {
2038
- // import com.example.Foo;
2039
- for (const child of n.namedChildren) {
2040
- if (child.type === "scoped_identifier") {
2041
- const name = child.text.split(".").pop() ?? child.text;
2042
- edges.push({
2043
- from_name: "__file__",
2044
- to_name: name,
2045
- type: "imports",
2046
- import_source: child.text,
2047
- });
2048
- }
2049
- }
2050
- }
2051
- if (n.type === "class_declaration") {
2052
- const name = n.childForFieldName("name");
2053
- const superclass = n.childForFieldName("superclass");
2054
- const interfaces = n.childForFieldName("interfaces");
2055
- if (name && superclass) {
2056
- edges.push({
2057
- from_name: name.text,
2058
- to_name: superclass.text,
2059
- type: "extends",
2060
- });
2061
- }
2062
- if (name && interfaces) {
2063
- for (const iface of interfaces.namedChildren) {
2064
- if (iface.type === "type_identifier" || iface.type === "type_list") {
2065
- const names = iface.type === "type_list" ? iface.namedChildren : [iface];
2066
- for (const t of names) {
2067
- edges.push({
2068
- from_name: name.text,
2069
- to_name: t.text,
2070
- type: "implements",
2071
- });
2072
- }
2073
- }
2074
- }
2075
- }
2076
- }
2077
- });
2078
- }
2079
- /** Rust: extract use statements and trait impls. */
2080
- function extractRustEdges(root, edges) {
2081
- walkNodes(root, (n) => {
2082
- if (n.type === "use_declaration") {
2083
- // Extract the full use path for import_source, then leaf names
2084
- const fullPath = extractRustUsePath(n);
2085
- extractRustUseNames(n, edges, fullPath);
2086
- }
2087
- // impl Trait for Type
2088
- if (n.type === "impl_item") {
2089
- const traitNode = n.childForFieldName("trait");
2090
- const typeNode = n.childForFieldName("type");
2091
- if (traitNode && typeNode) {
2092
- edges.push({
2093
- from_name: typeNode.text,
2094
- to_name: traitNode.text,
2095
- type: "implements",
2096
- });
2097
- }
2098
- }
2099
- });
2100
- }
2101
- // ── New language edge extraction (tree-sitter) ──────────────────
2102
- function extractCSharpEdges(root, edges) {
2103
- walkNodes(root, (n) => {
2104
- // using Namespace.Type;
2105
- if (n.type === "using_directive") {
2106
- const name = n.text
2107
- .replace(/^using\s+/, "")
2108
- .replace(/;$/, "")
2109
- .trim();
2110
- const leaf = name.split(".").pop() ?? name;
2111
- edges.push({
2112
- from_name: "__file__",
2113
- to_name: leaf,
2114
- type: "imports",
2115
- import_source: name,
2116
- });
2117
- }
2118
- // class Foo : Bar, IBaz
2119
- if (n.type === "base_list") {
2120
- const parent = n.parent;
2121
- const parentName = parent?.childForFieldName("name");
2122
- if (parentName) {
2123
- for (const child of n.namedChildren) {
2124
- const baseName = child.type === "identifier"
2125
- ? child.text
2126
- : child.childForFieldName("name")?.text;
2127
- if (baseName) {
2128
- edges.push({
2129
- from_name: parentName.text,
2130
- to_name: baseName,
2131
- type: "extends",
2132
- });
2133
- }
2134
- }
2135
- }
2136
- }
2137
- });
2138
- }
2139
- function extractRubyEdges(root, edges) {
2140
- walkNodes(root, (n) => {
2141
- // require "foo" / require_relative "foo"
2142
- if (n.type === "call" && n.namedChildren.length >= 1) {
2143
- const methodName = n.childForFieldName("method");
2144
- if (methodName &&
2145
- (methodName.text === "require" ||
2146
- methodName.text === "require_relative")) {
2147
- const args = n.childForFieldName("arguments");
2148
- if (args) {
2149
- for (const arg of args.namedChildren) {
2150
- if (arg.type === "string") {
2151
- const val = arg.text.replace(/^['"]|['"]$/g, "");
2152
- const leaf = val.split("/").pop() ?? val;
2153
- edges.push({
2154
- from_name: "__file__",
2155
- to_name: leaf,
2156
- type: "imports",
2157
- import_source: val,
2158
- });
2159
- }
2160
- }
2161
- }
2162
- }
2163
- }
2164
- // class Foo < Bar
2165
- if (n.type === "class") {
2166
- const name = n.childForFieldName("name");
2167
- const superclass = n.childForFieldName("superclass");
2168
- if (name && superclass) {
2169
- edges.push({
2170
- from_name: name.text,
2171
- to_name: superclass.text,
2172
- type: "extends",
2173
- });
2174
- }
2175
- }
2176
- });
2177
- }
2178
- function extractPHPEdges(root, edges) {
2179
- walkNodes(root, (n) => {
2180
- // use Namespace\Class;
2181
- if (n.type === "namespace_use_declaration") {
2182
- for (const clause of n.namedChildren) {
2183
- if (clause.type === "namespace_use_clause") {
2184
- const name = clause.text;
2185
- const leaf = name.split("\\").pop() ?? name;
2186
- edges.push({
2187
- from_name: "__file__",
2188
- to_name: leaf,
2189
- type: "imports",
2190
- import_source: name,
2191
- });
2192
- }
2193
- }
2194
- }
2195
- // class Foo extends Bar implements Baz
2196
- if (n.type === "class_declaration") {
2197
- const name = n.childForFieldName("name");
2198
- if (!name)
2199
- return;
2200
- const baseClause = n.childForFieldName("base_clause");
2201
- if (baseClause) {
2202
- for (const child of baseClause.namedChildren) {
2203
- if (child.type === "name" || child.type === "qualified_name") {
2204
- edges.push({
2205
- from_name: name.text,
2206
- to_name: child.text,
2207
- type: "extends",
2208
- });
2209
- }
2210
- }
2211
- }
2212
- const interfaces = n.childForFieldName("interfaces");
2213
- if (interfaces) {
2214
- for (const child of interfaces.namedChildren) {
2215
- if (child.type === "name" || child.type === "qualified_name") {
2216
- edges.push({
2217
- from_name: name.text,
2218
- to_name: child.text,
2219
- type: "implements",
2220
- });
2221
- }
2222
- }
2223
- }
2224
- }
2225
- });
2226
- }
2227
- function extractKotlinEdges(root, edges) {
2228
- walkNodes(root, (n) => {
2229
- // import foo.bar.Baz
2230
- if (n.type === "import_header") {
2231
- const path = n.text.replace(/^import\s+/, "").trim();
2232
- const leaf = path.split(".").pop() ?? path;
2233
- if (leaf !== "*") {
2234
- edges.push({
2235
- from_name: "__file__",
2236
- to_name: leaf,
2237
- type: "imports",
2238
- import_source: path,
2239
- });
2240
- }
2241
- }
2242
- // class Foo : Bar, Baz
2243
- if (n.type === "class_declaration" || n.type === "object_declaration") {
2244
- const name = findFirstIdentifier(n);
2245
- if (!name)
2246
- return;
2247
- const delegation = n.namedChildren.find((c) => c.type === "delegation_specifier" ||
2248
- c.type === "delegation_specifiers");
2249
- if (delegation) {
2250
- for (const child of delegation.namedChildren) {
2251
- const typeName = child.text.split("(")[0]?.trim();
2252
- if (typeName) {
2253
- edges.push({ from_name: name, to_name: typeName, type: "extends" });
2254
- }
2255
- }
2256
- }
2257
- }
2258
- });
2259
- }
2260
- function extractSwiftEdges(root, edges) {
2261
- walkNodes(root, (n) => {
2262
- // import Foundation
2263
- if (n.type === "import_declaration") {
2264
- const path = n.text.replace(/^import\s+/, "").trim();
2265
- const leaf = path.split(".").pop() ?? path;
2266
- edges.push({
2267
- from_name: "__file__",
2268
- to_name: leaf,
2269
- type: "imports",
2270
- import_source: path,
2271
- });
2272
- }
2273
- // class Foo: Bar, Baz — inheritance_specifier
2274
- if (n.type === "class_declaration" ||
2275
- n.type === "struct_declaration" ||
2276
- n.type === "enum_declaration") {
2277
- const name = n.childForFieldName("name");
2278
- if (!name)
2279
- return;
2280
- for (const child of n.namedChildren) {
2281
- if (child.type === "type_inheritance_clause") {
2282
- for (const inherited of child.namedChildren) {
2283
- const typeName = inherited.text.trim();
2284
- if (typeName) {
2285
- edges.push({
2286
- from_name: name.text,
2287
- to_name: typeName,
2288
- type: "extends",
2289
- });
2290
- }
2291
- }
2292
- }
2293
- }
2294
- }
2295
- });
2296
- }
2297
- /** Extract the full path from a Rust use declaration (e.g., "crate::module::sub"). */
2298
- function extractRustUsePath(node) {
2299
- // The use_declaration's text is like "use crate::foo::bar;" — extract the path portion
2300
- const text = node.text
2301
- .replace(/^use\s+/, "")
2302
- .replace(/;$/, "")
2303
- .trim();
2304
- // Remove any trailing ::{...} or ::* for the base path
2305
- const braceIdx = text.indexOf("::{");
2306
- if (braceIdx >= 0)
2307
- return text.slice(0, braceIdx);
2308
- const starIdx = text.indexOf("::*");
2309
- if (starIdx >= 0)
2310
- return text.slice(0, starIdx);
2311
- // For "use crate::foo::Bar" → base is "crate::foo"
2312
- const lastSep = text.lastIndexOf("::");
2313
- if (lastSep >= 0)
2314
- return text.slice(0, lastSep);
2315
- return text;
2316
- }
2317
- function extractRustUseNames(node, edges, importSource) {
2318
- walkNodes(node, (n) => {
2319
- if (n.type === "identifier" && n.parent?.type === "use_as_clause") {
2320
- edges.push({
2321
- from_name: "__file__",
2322
- to_name: n.text,
2323
- type: "imports",
2324
- import_source: importSource,
2325
- });
2326
- }
2327
- else if (n.type === "identifier" &&
2328
- (n.parent?.type === "use_declaration" ||
2329
- n.parent?.type === "scoped_identifier" ||
2330
- n.parent?.type === "use_list")) {
2331
- // Only leaf identifiers (no parent scoped_identifier that has this as left side)
2332
- const isLeaf = !n.children.some((c) => c.type === "scoped_identifier");
2333
- if (isLeaf && n.parent?.type !== "scoped_identifier") {
2334
- edges.push({
2335
- from_name: "__file__",
2336
- to_name: n.text,
2337
- type: "imports",
2338
- import_source: importSource,
2339
- });
2340
- }
2341
- }
2342
- });
2343
- }
2344
- /**
2345
- * Find the enclosing entity (function/method/class) for a given AST node.
2346
- * Returns the entity name or null if at file scope.
2347
- */
2348
- function findEnclosingEntity(node, entities) {
2349
- const callLine = node.startPosition.row + 1;
2350
- let best = null;
2351
- let bestSpan = Number.POSITIVE_INFINITY;
2352
- for (const entity of entities) {
2353
- if (callLine >= entity.line_start && callLine <= entity.line_end) {
2354
- const span = entity.line_end - entity.line_start;
2355
- // Prefer the narrowest enclosing entity (method over class)
2356
- if (span < bestSpan) {
2357
- bestSpan = span;
2358
- best = entity;
2359
- }
2360
- }
2361
- }
2362
- return best?.name ?? null;
2363
- }
2364
- /** Regex-based edge extraction fallback (imports only). */
2365
- function extractEdgesRegex(content, _filePath, language) {
2366
- const edges = [];
2367
- const lines = content.split("\n");
2368
- for (const line of lines) {
2369
- switch (language) {
2370
- case "typescript":
2371
- case "javascript": {
2372
- // import { Foo, Bar } from "./module"
2373
- const importMatch = line.match(/^\s*import\s+(?:(?:type\s+)?{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/);
2374
- if (importMatch) {
2375
- const names = importMatch[1] ?? importMatch[2] ?? "";
2376
- const source = importMatch[3] ?? "";
2377
- for (const name of names.split(",")) {
2378
- const trimmed = name
2379
- .trim()
2380
- .split(/\s+as\s+/)[0]
2381
- ?.trim();
2382
- if (trimmed) {
2383
- edges.push({
2384
- from_name: "__file__",
2385
- to_name: trimmed,
2386
- type: "imports",
2387
- import_source: source,
2388
- });
2389
- }
2390
- }
2391
- }
2392
- // class Foo extends Bar
2393
- const extendsMatch = line.match(/class\s+(\w+)\s+extends\s+(\w+)/);
2394
- if (extendsMatch?.[1] && extendsMatch[2]) {
2395
- edges.push({
2396
- from_name: extendsMatch[1],
2397
- to_name: extendsMatch[2],
2398
- type: "extends",
2399
- });
2400
- }
2401
- break;
2402
- }
2403
- case "python": {
2404
- const fromImport = line.match(/^\s*from\s+(\S+)\s+import\s+(.+)/);
2405
- if (fromImport?.[1] && fromImport[2]) {
2406
- const pySource = fromImport[1];
2407
- for (const name of fromImport[2].split(",")) {
2408
- const trimmed = name
2409
- .trim()
2410
- .split(/\s+as\s+/)[0]
2411
- ?.trim();
2412
- if (trimmed) {
2413
- edges.push({
2414
- from_name: "__file__",
2415
- to_name: trimmed,
2416
- type: "imports",
2417
- import_source: pySource,
2418
- });
2419
- }
2420
- }
2421
- }
2422
- const pyImport = line.match(/^\s*import\s+(\S+)/);
2423
- if (pyImport?.[1] && !line.includes(" from ")) {
2424
- const mod = pyImport[1].replace(/,$/, "");
2425
- edges.push({
2426
- from_name: "__file__",
2427
- to_name: mod.split(".").pop() ?? mod,
2428
- type: "imports",
2429
- import_source: mod,
2430
- });
2431
- }
2432
- break;
2433
- }
2434
- case "go": {
2435
- const goImport = line.match(/^\s*"([^"]+)"/);
2436
- if (goImport?.[1]) {
2437
- const pkg = goImport[1].split("/").pop() ?? goImport[1];
2438
- edges.push({
2439
- from_name: "__file__",
2440
- to_name: pkg,
2441
- type: "imports",
2442
- import_source: goImport[1],
2443
- });
2444
- }
2445
- break;
2446
- }
2447
- case "java": {
2448
- const javaImport = line.match(/^\s*import\s+(?:static\s+)?([^;]+);/);
2449
- if (javaImport?.[1]) {
2450
- const name = javaImport[1].split(".").pop() ?? javaImport[1];
2451
- edges.push({
2452
- from_name: "__file__",
2453
- to_name: name,
2454
- type: "imports",
2455
- import_source: javaImport[1],
2456
- });
2457
- }
2458
- break;
2459
- }
2460
- case "rust": {
2461
- const useMatch = line.match(/^\s*use\s+([^;]+);/);
2462
- if (useMatch?.[1]) {
2463
- const path = useMatch[1];
2464
- const name = path.split("::").pop() ?? path;
2465
- // Compute base path (everything before last ::)
2466
- const lastSep = path.lastIndexOf("::");
2467
- const rustSource = lastSep >= 0 ? path.slice(0, lastSep) : path;
2468
- if (name !== "*" && name !== "self") {
2469
- edges.push({
2470
- from_name: "__file__",
2471
- to_name: name,
2472
- type: "imports",
2473
- import_source: rustSource,
2474
- });
2475
- }
2476
- }
2477
- break;
2478
- }
2479
- case "csharp": {
2480
- const usingMatch = line.match(/^\s*using\s+(?:static\s+)?([^;=]+);/);
2481
- if (usingMatch?.[1]) {
2482
- const ns = usingMatch[1].trim();
2483
- const leaf = ns.split(".").pop() ?? ns;
2484
- edges.push({
2485
- from_name: "__file__",
2486
- to_name: leaf,
2487
- type: "imports",
2488
- import_source: ns,
2489
- });
2490
- }
2491
- const csExtends = line.match(/class\s+(\w+)\s*:\s*(\w+)/);
2492
- if (csExtends?.[1] && csExtends[2]) {
2493
- edges.push({
2494
- from_name: csExtends[1],
2495
- to_name: csExtends[2],
2496
- type: "extends",
2497
- });
2498
- }
2499
- break;
2500
- }
2501
- case "ruby": {
2502
- const reqMatch = line.match(/^\s*require(?:_relative)?\s+['"]([^'"]+)['"]/);
2503
- if (reqMatch?.[1]) {
2504
- const mod = reqMatch[1];
2505
- const leaf = mod.split("/").pop() ?? mod;
2506
- edges.push({
2507
- from_name: "__file__",
2508
- to_name: leaf,
2509
- type: "imports",
2510
- import_source: mod,
2511
- });
2512
- }
2513
- const rbExtends = line.match(/class\s+(\w+)\s*<\s*(\w+)/);
2514
- if (rbExtends?.[1] && rbExtends[2]) {
2515
- edges.push({
2516
- from_name: rbExtends[1],
2517
- to_name: rbExtends[2],
2518
- type: "extends",
2519
- });
2520
- }
2521
- break;
2522
- }
2523
- case "php": {
2524
- const phpUse = line.match(/^\s*use\s+([^;]+);/);
2525
- if (phpUse?.[1]) {
2526
- const ns = phpUse[1].trim();
2527
- const leaf = ns.split("\\").pop() ?? ns;
2528
- edges.push({
2529
- from_name: "__file__",
2530
- to_name: leaf,
2531
- type: "imports",
2532
- import_source: ns,
2533
- });
2534
- }
2535
- const phpExtends = line.match(/class\s+(\w+)\s+extends\s+(\w+)/);
2536
- if (phpExtends?.[1] && phpExtends[2]) {
2537
- edges.push({
2538
- from_name: phpExtends[1],
2539
- to_name: phpExtends[2],
2540
- type: "extends",
2541
- });
2542
- }
2543
- const phpImpl = line.match(/class\s+(\w+)\s+(?:extends\s+\w+\s+)?implements\s+(.+?)(?:\s*\{|$)/);
2544
- if (phpImpl?.[1] && phpImpl[2]) {
2545
- for (const iface of phpImpl[2].split(",")) {
2546
- const trimmed = iface.trim();
2547
- if (trimmed)
2548
- edges.push({
2549
- from_name: phpImpl[1],
2550
- to_name: trimmed,
2551
- type: "implements",
2552
- });
2553
- }
2554
- }
2555
- break;
2556
- }
2557
- case "kotlin": {
2558
- const ktImport = line.match(/^\s*import\s+(\S+)/);
2559
- if (ktImport?.[1]) {
2560
- const path = ktImport[1];
2561
- const leaf = path.split(".").pop() ?? path;
2562
- if (leaf !== "*") {
2563
- edges.push({
2564
- from_name: "__file__",
2565
- to_name: leaf,
2566
- type: "imports",
2567
- import_source: path,
2568
- });
2569
- }
2570
- }
2571
- const ktExtends = line.match(/class\s+(\w+)\s*(?:\([^)]*\))?\s*:\s*(\w+)/);
2572
- if (ktExtends?.[1] && ktExtends[2]) {
2573
- edges.push({
2574
- from_name: ktExtends[1],
2575
- to_name: ktExtends[2],
2576
- type: "extends",
2577
- });
2578
- }
2579
- break;
2580
- }
2581
- case "swift": {
2582
- const swImport = line.match(/^\s*import\s+(\S+)/);
2583
- if (swImport?.[1]) {
2584
- const mod = swImport[1];
2585
- edges.push({
2586
- from_name: "__file__",
2587
- to_name: mod,
2588
- type: "imports",
2589
- import_source: mod,
2590
- });
2591
- }
2592
- const swExtends = line.match(/(?:class|struct|enum)\s+(\w+)\s*:\s*(\w+)/);
2593
- if (swExtends?.[1] && swExtends[2]) {
2594
- edges.push({
2595
- from_name: swExtends[1],
2596
- to_name: swExtends[2],
2597
- type: "extends",
2598
- });
2599
- }
2600
- break;
2601
- }
2602
- default:
2603
- break;
2604
- }
2605
- }
2606
- return edges;
2607
- }
2608
- // ── AST walker utility ──────────────────────────────────────────
2609
- /** Depth-first walk of tree-sitter AST, calling visitor on each node. */
2610
- function walkNodes(node, visitor) {
2611
- visitor(node);
2612
- for (const child of node.children) {
2613
- walkNodes(child, visitor);
2614
- }
2615
- }