gitnexus 1.6.3-rc.8 → 1.6.3

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 (285) hide show
  1. package/README.md +21 -5
  2. package/dist/_shared/graph/types.d.ts +16 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +20 -2
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/index.js +11 -0
  7. package/dist/_shared/index.js.map +1 -1
  8. package/dist/_shared/scope-resolution/def-index.js +2 -2
  9. package/dist/_shared/scope-resolution/def-index.js.map +1 -1
  10. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
  11. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
  12. package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
  13. package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
  14. package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
  15. package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
  16. package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
  17. package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
  18. package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
  19. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
  20. package/dist/_shared/scope-resolution/parsed-file.js +54 -0
  21. package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
  22. package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
  23. package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
  24. package/dist/_shared/scope-resolution/position-index.js +2 -2
  25. package/dist/_shared/scope-resolution/position-index.js.map +1 -1
  26. package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
  27. package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
  28. package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
  29. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
  30. package/dist/_shared/scope-resolution/reference-site.js +24 -0
  31. package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
  32. package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
  33. package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
  34. package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
  35. package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
  36. package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
  37. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
  38. package/dist/_shared/scope-resolution/registries/context.js +44 -0
  39. package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
  40. package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
  41. package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
  42. package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
  43. package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
  44. package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
  45. package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
  46. package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
  47. package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
  48. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
  49. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
  50. package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
  51. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
  52. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
  53. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
  54. package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
  55. package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
  56. package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
  57. package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
  58. package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
  59. package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
  60. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
  61. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
  62. package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
  63. package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
  64. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
  65. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
  66. package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
  67. package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
  68. package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
  69. package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
  70. package/dist/_shared/scope-resolution/scope-tree.js +3 -2
  71. package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
  72. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
  73. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
  74. package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
  75. package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
  76. package/dist/_shared/scope-resolution/types.d.ts +11 -0
  77. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  78. package/dist/cli/ai-context.js +35 -4
  79. package/dist/cli/analyze.d.ts +27 -0
  80. package/dist/cli/analyze.js +31 -1
  81. package/dist/cli/clean.js +19 -1
  82. package/dist/cli/group.js +73 -0
  83. package/dist/cli/index-repo.js +8 -1
  84. package/dist/cli/index.js +26 -1
  85. package/dist/cli/list.js +11 -1
  86. package/dist/cli/remove.d.ts +30 -0
  87. package/dist/cli/remove.js +99 -0
  88. package/dist/cli/setup.js +185 -57
  89. package/dist/cli/tool.d.ts +5 -0
  90. package/dist/cli/tool.js +42 -0
  91. package/dist/config/ignore-service.d.ts +9 -0
  92. package/dist/config/ignore-service.js +80 -13
  93. package/dist/core/embedding-mode.d.ts +30 -0
  94. package/dist/core/embedding-mode.js +30 -0
  95. package/dist/core/embeddings/ast-utils.js +22 -22
  96. package/dist/core/embeddings/chunker.js +30 -25
  97. package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
  98. package/dist/core/embeddings/embedding-pipeline.js +15 -6
  99. package/dist/core/embeddings/text-generator.d.ts +1 -1
  100. package/dist/core/embeddings/text-generator.js +33 -24
  101. package/dist/core/embeddings/types.d.ts +43 -1
  102. package/dist/core/embeddings/types.js +101 -29
  103. package/dist/core/git-staleness.d.ts +18 -0
  104. package/dist/core/git-staleness.js +108 -0
  105. package/dist/core/graph/graph.js +115 -20
  106. package/dist/core/graph/types.d.ts +12 -1
  107. package/dist/core/group/config-parser.d.ts +4 -0
  108. package/dist/core/group/config-parser.js +18 -1
  109. package/dist/core/group/cross-impact.d.ts +41 -0
  110. package/dist/core/group/cross-impact.js +441 -0
  111. package/dist/core/group/extractors/http-patterns/php.js +126 -18
  112. package/dist/core/group/group-path-utils.d.ts +17 -0
  113. package/dist/core/group/group-path-utils.js +40 -0
  114. package/dist/core/group/resolve-at-member.d.ts +10 -0
  115. package/dist/core/group/resolve-at-member.js +31 -0
  116. package/dist/core/group/service.d.ts +9 -0
  117. package/dist/core/group/service.js +259 -25
  118. package/dist/core/group/types.d.ts +30 -0
  119. package/dist/core/ingestion/ast-cache.d.ts +16 -1
  120. package/dist/core/ingestion/ast-cache.js +14 -2
  121. package/dist/core/ingestion/call-processor.js +9 -0
  122. package/dist/core/ingestion/emit-references.d.ts +88 -0
  123. package/dist/core/ingestion/emit-references.js +229 -0
  124. package/dist/core/ingestion/filesystem-walker.js +6 -4
  125. package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
  126. package/dist/core/ingestion/finalize-orchestrator.js +139 -0
  127. package/dist/core/ingestion/framework-detection.js +6 -2
  128. package/dist/core/ingestion/import-processor.js +4 -0
  129. package/dist/core/ingestion/import-resolvers/python.js +9 -6
  130. package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
  131. package/dist/core/ingestion/import-target-adapter.js +95 -0
  132. package/dist/core/ingestion/language-provider.d.ts +36 -33
  133. package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
  134. package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
  135. package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
  136. package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
  137. package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
  138. package/dist/core/ingestion/languages/csharp/arity.js +37 -0
  139. package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
  140. package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
  141. package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
  142. package/dist/core/ingestion/languages/csharp/captures.js +249 -0
  143. package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
  144. package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
  145. package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
  146. package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
  147. package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
  148. package/dist/core/ingestion/languages/csharp/index.js +82 -0
  149. package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
  150. package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
  151. package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
  152. package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
  153. package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
  154. package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
  155. package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
  156. package/dist/core/ingestion/languages/csharp/query.js +515 -0
  157. package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
  158. package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
  159. package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
  160. package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
  161. package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
  162. package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
  163. package/dist/core/ingestion/languages/csharp.js +14 -0
  164. package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
  165. package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
  166. package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
  167. package/dist/core/ingestion/languages/python/arity.js +38 -0
  168. package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
  169. package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
  170. package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
  171. package/dist/core/ingestion/languages/python/captures.js +106 -0
  172. package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
  173. package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
  174. package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
  175. package/dist/core/ingestion/languages/python/import-target.js +99 -0
  176. package/dist/core/ingestion/languages/python/index.d.ts +80 -0
  177. package/dist/core/ingestion/languages/python/index.js +80 -0
  178. package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
  179. package/dist/core/ingestion/languages/python/interpret.js +191 -0
  180. package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
  181. package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
  182. package/dist/core/ingestion/languages/python/query.d.ts +9 -0
  183. package/dist/core/ingestion/languages/python/query.js +267 -0
  184. package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
  185. package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
  186. package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
  187. package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
  188. package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
  189. package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
  190. package/dist/core/ingestion/languages/python.js +14 -0
  191. package/dist/core/ingestion/model/method-registry.d.ts +9 -0
  192. package/dist/core/ingestion/model/method-registry.js +4 -0
  193. package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
  194. package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
  195. package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
  196. package/dist/core/ingestion/model/semantic-model.js +55 -0
  197. package/dist/core/ingestion/mro-processor.js +38 -22
  198. package/dist/core/ingestion/parsing-processor.d.ts +18 -1
  199. package/dist/core/ingestion/parsing-processor.js +45 -11
  200. package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
  201. package/dist/core/ingestion/pipeline-phases/index.js +1 -0
  202. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
  203. package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
  204. package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
  205. package/dist/core/ingestion/pipeline.js +2 -1
  206. package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
  207. package/dist/core/ingestion/registry-primary-flag.js +111 -0
  208. package/dist/core/ingestion/resolve-references.d.ts +63 -0
  209. package/dist/core/ingestion/resolve-references.js +175 -0
  210. package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
  211. package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
  212. package/dist/core/ingestion/scope-extractor.d.ts +86 -0
  213. package/dist/core/ingestion/scope-extractor.js +758 -0
  214. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
  215. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
  216. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
  217. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
  218. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
  219. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
  220. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
  221. package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
  222. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
  223. package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
  224. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
  225. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
  226. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
  227. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
  228. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
  229. package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
  230. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
  231. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
  232. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
  233. package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
  234. package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
  235. package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
  236. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
  237. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
  238. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
  239. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
  240. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
  241. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
  242. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
  243. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
  244. package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
  245. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
  246. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
  247. package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
  248. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
  249. package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
  250. package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
  251. package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
  252. package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
  253. package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
  254. package/dist/core/ingestion/shadow-harness.d.ts +113 -0
  255. package/dist/core/ingestion/shadow-harness.js +148 -0
  256. package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
  257. package/dist/core/ingestion/utils/ast-helpers.js +70 -0
  258. package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
  259. package/dist/core/ingestion/utils/max-file-size.js +52 -0
  260. package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
  261. package/dist/core/ingestion/workers/parse-worker.js +57 -21
  262. package/dist/core/lbug/lbug-adapter.d.ts +22 -2
  263. package/dist/core/lbug/lbug-adapter.js +58 -14
  264. package/dist/core/lbug/pool-adapter.d.ts +17 -0
  265. package/dist/core/lbug/pool-adapter.js +24 -14
  266. package/dist/core/run-analyze.d.ts +32 -0
  267. package/dist/core/run-analyze.js +74 -19
  268. package/dist/core/search/bm25-index.d.ts +18 -0
  269. package/dist/core/search/bm25-index.js +125 -12
  270. package/dist/core/tree-sitter/parser-loader.js +6 -1
  271. package/dist/mcp/local/local-backend.d.ts +67 -3
  272. package/dist/mcp/local/local-backend.js +296 -34
  273. package/dist/mcp/resources.d.ts +31 -0
  274. package/dist/mcp/resources.js +100 -17
  275. package/dist/mcp/tools.d.ts +4 -1
  276. package/dist/mcp/tools.js +75 -54
  277. package/dist/server/api.js +6 -2
  278. package/dist/storage/git.d.ts +49 -0
  279. package/dist/storage/git.js +111 -0
  280. package/dist/storage/repo-manager.d.ts +246 -1
  281. package/dist/storage/repo-manager.js +391 -9
  282. package/package.json +7 -6
  283. package/scripts/bench-scope-resolution.ts +134 -0
  284. package/scripts/ci-list-migrated-languages.ts +24 -0
  285. package/skills/gitnexus-cli.md +1 -0
@@ -0,0 +1,758 @@
1
+ /**
2
+ * `ScopeExtractor` — the central, source-agnostic driver that turns a
3
+ * language provider's `CaptureMatch[]` into a `ParsedFile`
4
+ * (RFC §5.3 + §3.2 Phase 1; Ring 2 PKG #919).
5
+ *
6
+ * Exactly one entry point: `extract(matches, filePath, provider) → ParsedFile`.
7
+ * Runs a five-pass pipeline over the matches. Each pass is internal; the
8
+ * public contract is the output `ParsedFile`.
9
+ *
10
+ * ## Design principles
11
+ *
12
+ * - **Source-agnostic.** Consumes `CaptureMatch[]` from providers;
13
+ * doesn't know whether they came from tree-sitter queries or COBOL's
14
+ * regex tagger. No `Tree` / `SyntaxNode` types leak into this file.
15
+ * - **One AST walk per language.** Providers do the AST walk inside
16
+ * their `emitScopeCaptures` hook; this driver does zero further
17
+ * traversal — it consumes captures only.
18
+ * - **Pure-ish.** The extractor itself is pure (same matches →
19
+ * same ParsedFile) when providers are pure. No side effects, no I/O.
20
+ * - **Centralized invariant enforcement.** Structural invariants on the
21
+ * scope tree (non-module has parent; parent contains child; siblings
22
+ * don't overlap) are enforced by `buildScopeTree` from Ring 2 SHARED
23
+ * (#912). Malformed inputs throw `ScopeTreeInvariantError`.
24
+ *
25
+ * ## The five passes
26
+ *
27
+ * 1. **Build scope tree.** Walk `@scope.*` matches. For each, consult
28
+ * `provider.resolveScopeKind` (default: suffix of the capture name).
29
+ * Derive parent by lexical-range containment. Hand the resulting
30
+ * `Scope[]` to `buildScopeTree` for validation.
31
+ * 2. **Attach declarations + local bindings.** Walk `@declaration.*`
32
+ * matches. For each, build a `SymbolDefinition` and attach it to
33
+ * `provider.bindingScopeFor` (default: innermost containing scope)
34
+ * as `ownedDefs` + a local `BindingRef { origin: 'local' }`.
35
+ * 3. **Collect raw imports.** Walk `@import.*` matches. Call
36
+ * `provider.interpretImport` per match; attach the returned
37
+ * `ParsedImport` to the ParsedFile (not to any `Scope` — finalize
38
+ * reconstructs the owning scope via `provider.importOwningScope`
39
+ * during Phase 2).
40
+ * 4. **Collect type bindings.** Walk `@type-binding.*` matches. Call
41
+ * `provider.interpretTypeBinding` per match. Attach the resulting
42
+ * `TypeRef` to the innermost containing scope's `typeBindings`
43
+ * (or override via `provider.bindingScopeFor` if set).
44
+ * 5. **Collect reference sites.** Walk `@reference.*` matches. Emit
45
+ * one `ReferenceSite` per match. Classify call form via
46
+ * `provider.classifyCallForm` (default: the capture's sub-tag if
47
+ * present; else `'free'`).
48
+ *
49
+ * ## What gets attached where
50
+ *
51
+ * - `Scope.bindings` — **local bindings only** at this stage (Pass 2).
52
+ * Finalize (#915) merges imports/wildcards on top.
53
+ * - `Scope.ownedDefs` — declarations structurally owned by this scope.
54
+ * - `Scope.typeBindings` — local type facts (parameter annotations, `self`).
55
+ * - `Scope.imports` — empty here. Populated by the finalize algorithm
56
+ * when it resolves `ParsedImport.targetRaw`.
57
+ * - `ParsedFile.parsedImports` — every raw import in this file.
58
+ * - `ParsedFile.localDefs` — flattened union of `Scope.ownedDefs`.
59
+ * - `ParsedFile.referenceSites` — pre-resolution usage facts.
60
+ */
61
+ import { buildPositionIndex, buildScopeTree, makeScopeId } from '../../_shared/index.js';
62
+ // ─── Public entry point ─────────────────────────────────────────────────────
63
+ /**
64
+ * Drive the five extraction passes and return a `ParsedFile`.
65
+ *
66
+ * Throws `ScopeTreeInvariantError` (from #912) when the provider emits
67
+ * captures that violate structural scope invariants. The error surfaces
68
+ * upward rather than being silently corrected — a malformed capture set
69
+ * is a bug in the provider's `emitScopeCaptures`, not a data condition
70
+ * to tolerate.
71
+ */
72
+ export function extract(matches, filePath, provider) {
73
+ // Partition matches by topic up front — one linear pass over the input.
74
+ const partitioned = partitionByTopic(matches);
75
+ // ── Pass 1: build the scope tree ─────────────────────────────────────
76
+ const scopeDrafts = pass1BuildScopes(partitioned.scope, filePath, provider);
77
+ const scopes = scopeDrafts.map(draftToScope);
78
+ // buildScopeTree validates invariants (throws on violation) and exposes
79
+ // the lookup contract consumed by Passes 2-5.
80
+ //
81
+ // **Snapshot semantics.** Both `scopeTree` and `positionIndex` are built
82
+ // from the post-Pass-1 `scopes` — parent/range/kind are accurate, but
83
+ // `bindings`, `ownedDefs`, and `typeBindings` are all empty here. Later
84
+ // passes write into the *drafts*, not into these snapshots; any hook
85
+ // that reads `scope.bindings` etc. via the `scopeTree` argument sees a
86
+ // structural view only. This is by design — hooks use scopeTree for
87
+ // "what's the parent chain?" queries, not for content queries.
88
+ const scopeTree = buildScopeTree(scopes);
89
+ const positionIndex = buildPositionIndex(scopes);
90
+ const moduleScope = scopeDrafts.find((s) => s.kind === 'Module');
91
+ if (moduleScope === undefined) {
92
+ throw new Error(`ScopeExtractor: no Module scope found for '${filePath}'. ` +
93
+ `Provider must emit at least one @scope.module capture per file.`);
94
+ }
95
+ // ── Pass 2: attach declarations + local bindings ────────────────────
96
+ const localDefs = [];
97
+ pass2AttachDeclarations(partitioned.declaration, scopeDrafts, positionIndex, localDefs, filePath, provider, scopeTree);
98
+ // ── Pass 3: collect raw imports ─────────────────────────────────────
99
+ const parsedImports = [];
100
+ pass3CollectImports(partitioned.import_, parsedImports, provider);
101
+ // ── Pass 4: collect type bindings ───────────────────────────────────
102
+ pass4CollectTypeBindings(partitioned.typeBinding, scopeDrafts, positionIndex, filePath, provider, scopeTree);
103
+ // ── Pass 5: collect reference sites ─────────────────────────────────
104
+ const referenceSites = [];
105
+ pass5CollectReferences(partitioned.reference, positionIndex, filePath, referenceSites, provider, scopeTree);
106
+ // Freeze Scope drafts into final shape and return.
107
+ const frozenScopes = scopeDrafts.map(draftToScope);
108
+ return Object.freeze({
109
+ filePath,
110
+ moduleScope: moduleScope.id,
111
+ scopes: Object.freeze(frozenScopes),
112
+ parsedImports: Object.freeze(parsedImports.slice()),
113
+ localDefs: Object.freeze(localDefs.slice()),
114
+ referenceSites: Object.freeze(referenceSites.slice()),
115
+ });
116
+ }
117
+ /**
118
+ * Bucket each match by the topic of its anchor capture. The anchor is the
119
+ * capture whose name is prefixed with the match's topic (`@scope.*`,
120
+ * `@declaration.*`, `@import.*`, `@type-binding.*`, `@reference.*`).
121
+ *
122
+ * A match may contain additional captures (e.g., `@import.source`,
123
+ * `@declaration.class.name`) that are used by the provider hooks to
124
+ * decode details. Those live inside the `CaptureMatch` and are surfaced
125
+ * to hooks verbatim — the extractor itself only routes by anchor.
126
+ */
127
+ function partitionByTopic(matches) {
128
+ const scope = [];
129
+ const declaration = [];
130
+ const import_ = [];
131
+ const typeBinding = [];
132
+ const reference = [];
133
+ for (const match of matches) {
134
+ const topic = topicOf(match);
135
+ switch (topic) {
136
+ case 'scope':
137
+ scope.push(match);
138
+ break;
139
+ case 'declaration':
140
+ declaration.push(match);
141
+ break;
142
+ case 'import':
143
+ import_.push(match);
144
+ break;
145
+ case 'type-binding':
146
+ typeBinding.push(match);
147
+ break;
148
+ case 'reference':
149
+ reference.push(match);
150
+ break;
151
+ case 'unknown':
152
+ // Unrecognized anchor — silently skip. Providers may emit extra
153
+ // captures (e.g., `@comment`) that the extractor has no topic for.
154
+ break;
155
+ }
156
+ }
157
+ return { scope, declaration, import_, typeBinding, reference };
158
+ }
159
+ function topicOf(match) {
160
+ // The anchor is the capture whose name uses one of the known topic
161
+ // prefixes. For multi-capture matches, ALL captures share the topic;
162
+ // we pick the first matching key for efficiency.
163
+ for (const name of Object.keys(match)) {
164
+ if (name.startsWith('@scope.'))
165
+ return 'scope';
166
+ if (name.startsWith('@declaration.'))
167
+ return 'declaration';
168
+ if (name.startsWith('@import.'))
169
+ return 'import';
170
+ if (name.startsWith('@type-binding.'))
171
+ return 'type-binding';
172
+ if (name.startsWith('@reference.'))
173
+ return 'reference';
174
+ }
175
+ return 'unknown';
176
+ }
177
+ function draftToScope(draft) {
178
+ const frozenBindings = new Map();
179
+ for (const [name, refs] of draft.bindings) {
180
+ frozenBindings.set(name, Object.freeze(refs.slice()));
181
+ }
182
+ return {
183
+ id: draft.id,
184
+ parent: draft.parent,
185
+ kind: draft.kind,
186
+ range: draft.range,
187
+ filePath: draft.filePath,
188
+ bindings: frozenBindings,
189
+ ownedDefs: Object.freeze(draft.ownedDefs.slice()),
190
+ imports: Object.freeze(draft.imports.slice()),
191
+ typeBindings: new Map(draft.typeBindings),
192
+ };
193
+ }
194
+ // ─── Pass 1: build scope tree ──────────────────────────────────────────────
195
+ /**
196
+ * Convert `@scope.*` matches into `ScopeDraft[]`. Parent relationships
197
+ * are derived from range containment (outermost scope containing `range`
198
+ * becomes the parent).
199
+ */
200
+ function pass1BuildScopes(matches, filePath, provider) {
201
+ const candidates = [];
202
+ for (const match of matches) {
203
+ const anchor = anchorCaptureFor(match, '@scope.');
204
+ if (anchor === undefined)
205
+ continue;
206
+ const kind = resolveKindForScopeMatch(match, anchor, provider);
207
+ if (kind === null)
208
+ continue;
209
+ const id = makeScopeId({ filePath, range: anchor.range, kind });
210
+ candidates.push({ match, range: anchor.range, kind, id });
211
+ }
212
+ // Sort by (startLine, startCol) ASC, (endLine, endCol) DESC so outer
213
+ // scopes appear before their children for parent-resolution.
214
+ candidates.sort((a, b) => {
215
+ if (a.range.startLine !== b.range.startLine)
216
+ return a.range.startLine - b.range.startLine;
217
+ if (a.range.startCol !== b.range.startCol)
218
+ return a.range.startCol - b.range.startCol;
219
+ if (a.range.endLine !== b.range.endLine)
220
+ return b.range.endLine - a.range.endLine;
221
+ return b.range.endCol - a.range.endCol;
222
+ });
223
+ const drafts = [];
224
+ const stack = []; // enclosing real scopes, outermost at [0]
225
+ for (const cand of candidates) {
226
+ // Pop the stack until the top strictly contains this candidate.
227
+ while (stack.length > 0 && !rangeStrictlyContains(stack[stack.length - 1].range, cand.range)) {
228
+ stack.pop();
229
+ }
230
+ const parent = stack.length > 0 ? stack[stack.length - 1].id : null;
231
+ drafts.push(makeDraft(cand.id, parent, cand.kind, cand.range, filePath));
232
+ stack.push(cand);
233
+ }
234
+ return drafts;
235
+ }
236
+ function resolveKindForScopeMatch(match, anchor, provider) {
237
+ // Provider override takes precedence.
238
+ const override = provider.resolveScopeKind?.(match);
239
+ if (override !== undefined && override !== null)
240
+ return override;
241
+ // Default: derive from capture name suffix (`@scope.function` → 'Function').
242
+ const suffix = anchor.name.slice('@scope.'.length);
243
+ switch (suffix.toLowerCase()) {
244
+ case 'module':
245
+ return 'Module';
246
+ case 'namespace':
247
+ return 'Namespace';
248
+ case 'class':
249
+ return 'Class';
250
+ case 'function':
251
+ return 'Function';
252
+ case 'block':
253
+ return 'Block';
254
+ case 'expression':
255
+ return 'Expression';
256
+ default:
257
+ return null;
258
+ }
259
+ }
260
+ function makeDraft(id, parent, kind, range, filePath) {
261
+ return {
262
+ id,
263
+ parent,
264
+ kind,
265
+ range,
266
+ filePath,
267
+ bindings: new Map(),
268
+ ownedDefs: [],
269
+ imports: [],
270
+ typeBindings: new Map(),
271
+ };
272
+ }
273
+ // ─── Pass 2: attach declarations + local bindings ──────────────────────────
274
+ function pass2AttachDeclarations(matches, drafts, positionIndex, localDefs, filePath, provider, scopeTree) {
275
+ const draftById = new Map();
276
+ for (const d of drafts)
277
+ draftById.set(d.id, d);
278
+ for (const match of matches) {
279
+ const anchor = anchorCaptureFor(match, '@declaration.');
280
+ if (anchor === undefined)
281
+ continue;
282
+ const def = buildDefFromDeclarationMatch(match, anchor, filePath);
283
+ if (def === undefined)
284
+ continue;
285
+ // Find the innermost scope that contains the declaration's anchor range.
286
+ const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
287
+ if (innermostId === undefined)
288
+ continue;
289
+ const innermost = draftById.get(innermostId);
290
+ if (innermost === undefined)
291
+ continue;
292
+ // Ownership: attach the def to the innermost scope's `ownedDefs` — that
293
+ // is the structural owner. `def.ownerId` is NOT populated here — the
294
+ // extractor has no clean path to the parent's own DefId mid-extraction
295
+ // (the parent declaration may not yet have been processed, or may live
296
+ // in a different scope entirely). Providers that need `ownerId` should
297
+ // set it directly from the declaration hook (e.g., derive from the
298
+ // `@declaration.owner` capture or the parent scope id); otherwise
299
+ // `finalize` populates method/field `ownerId` via `MethodDispatchIndex`
300
+ // (#914) in a follow-up pass that sees every def already in place.
301
+ innermost.ownedDefs.push(def);
302
+ localDefs.push(def);
303
+ // Binding visibility: default to innermost; allow hoisting via
304
+ // `provider.bindingScopeFor`. `draftToScope(innermost)` here is a
305
+ // **structural** snapshot — parent/range/kind only. Hooks MUST NOT
306
+ // rely on `scope.bindings`, `ownedDefs`, or `typeBindings` being
307
+ // populated during Pass 2: those fields are written across passes,
308
+ // so reading them mid-extraction yields a partial view. The
309
+ // `scopeTree` argument is similarly snapshot-before-mutation.
310
+ //
311
+ // Auto-hoist for scope-creating declarations: when the declaration's
312
+ // anchor range is the same node that produced `innermost` (e.g. a
313
+ // `function_definition` is both `@scope.function` and the
314
+ // `@declaration.function` anchor), the name is visible OUTSIDE the
315
+ // body, not inside. Hoisting to the parent scope is what every
316
+ // mainstream language wants for function/class declarations. Hooks
317
+ // can override by returning a non-null scope id.
318
+ const autoHostedId = innermost.parent !== null && rangesEqual(anchor.range, innermost.range)
319
+ ? innermost.parent
320
+ : innermost.id;
321
+ const bindingScopeId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? autoHostedId;
322
+ const bindingHost = draftById.get(bindingScopeId) ?? innermost;
323
+ const nameKey = deriveDeclarationName(match, def);
324
+ if (nameKey === undefined)
325
+ continue;
326
+ const existing = bindingHost.bindings.get(nameKey) ?? [];
327
+ existing.push({ def, origin: 'local' });
328
+ bindingHost.bindings.set(nameKey, existing);
329
+ }
330
+ }
331
+ function buildDefFromDeclarationMatch(match, anchor, filePath) {
332
+ // Anchor name pattern: `@declaration.<kind>` where <kind> maps to NodeLabel.
333
+ const kindStr = anchor.name.slice('@declaration.'.length);
334
+ const type = normalizeNodeLabel(kindStr);
335
+ if (type === undefined)
336
+ return undefined;
337
+ const nameCap = match['@declaration.name'] ?? match[`@declaration.${kindStr}.name`] ?? match[anchor.name];
338
+ if (nameCap === undefined)
339
+ return undefined;
340
+ const qualifiedCap = match['@declaration.qualified_name'];
341
+ const qualifiedName = qualifiedCap?.text;
342
+ // Optional arity metadata — producers (e.g. Python emit-captures)
343
+ // synthesize these on function/method declarations. Their absence is
344
+ // the normal case for other producers; readers treat undefined as
345
+ // "unknown" per `SymbolDefinition` contract.
346
+ const parameterCount = parseIntCapture(match['@declaration.parameter-count']);
347
+ const requiredParameterCount = parseIntCapture(match['@declaration.required-parameter-count']);
348
+ const parameterTypes = parseJsonStringArrayCapture(match['@declaration.parameter-types']);
349
+ return {
350
+ nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
351
+ filePath,
352
+ type,
353
+ ...(qualifiedName !== undefined ? { qualifiedName } : { qualifiedName: nameCap.text }),
354
+ ...(parameterCount !== undefined ? { parameterCount } : {}),
355
+ ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
356
+ ...(parameterTypes !== undefined ? { parameterTypes } : {}),
357
+ };
358
+ }
359
+ function parseIntCapture(cap) {
360
+ if (cap === undefined)
361
+ return undefined;
362
+ const n = Number.parseInt(cap.text, 10);
363
+ return Number.isFinite(n) ? n : undefined;
364
+ }
365
+ function parseJsonStringArrayCapture(cap) {
366
+ if (cap === undefined)
367
+ return undefined;
368
+ try {
369
+ const parsed = JSON.parse(cap.text);
370
+ if (!Array.isArray(parsed))
371
+ return undefined;
372
+ return parsed.every((x) => typeof x === 'string') ? parsed : undefined;
373
+ }
374
+ catch {
375
+ return undefined;
376
+ }
377
+ }
378
+ function deriveDeclarationName(match, def) {
379
+ const nameCap = match['@declaration.name'] ??
380
+ match[Object.keys(match).find((k) => k.startsWith('@declaration.') && k.endsWith('.name')) ?? ''];
381
+ if (nameCap !== undefined)
382
+ return nameCap.text;
383
+ // Fall back to qualifiedName tail.
384
+ const q = def.qualifiedName;
385
+ if (q !== undefined && q.length > 0) {
386
+ const dot = q.lastIndexOf('.');
387
+ return dot === -1 ? q : q.slice(dot + 1);
388
+ }
389
+ return undefined;
390
+ }
391
+ /**
392
+ * Map a lower-case declaration kind (from `@declaration.<kind>`) to a
393
+ * graph `NodeLabel`. Silently returns `undefined` for kinds we don't
394
+ * recognize — providers can emit richer captures without breaking the
395
+ * driver.
396
+ */
397
+ function normalizeNodeLabel(kindStr) {
398
+ switch (kindStr.toLowerCase()) {
399
+ case 'class':
400
+ return 'Class';
401
+ case 'interface':
402
+ return 'Interface';
403
+ case 'enum':
404
+ return 'Enum';
405
+ case 'struct':
406
+ return 'Struct';
407
+ case 'union':
408
+ return 'Union';
409
+ case 'trait':
410
+ return 'Trait';
411
+ case 'method':
412
+ return 'Method';
413
+ case 'function':
414
+ return 'Function';
415
+ case 'constructor':
416
+ return 'Constructor';
417
+ case 'field':
418
+ case 'property':
419
+ return 'Property';
420
+ case 'variable':
421
+ case 'const':
422
+ return 'Variable';
423
+ case 'typealias':
424
+ case 'type_alias':
425
+ return 'TypeAlias';
426
+ case 'typedef':
427
+ return 'Typedef';
428
+ case 'record':
429
+ return 'Record';
430
+ case 'delegate':
431
+ return 'Delegate';
432
+ case 'annotation':
433
+ return 'Annotation';
434
+ case 'namespace':
435
+ return 'Namespace';
436
+ default:
437
+ return undefined;
438
+ }
439
+ }
440
+ function makeDefId(filePath, range, type, name) {
441
+ return `def:${filePath}#${range.startLine}:${range.startCol}:${type}:${name}`;
442
+ }
443
+ // ─── Pass 3: collect raw imports ───────────────────────────────────────────
444
+ function pass3CollectImports(matches, parsedImports, provider) {
445
+ if (provider.interpretImport === undefined)
446
+ return;
447
+ for (const match of matches) {
448
+ const anchor = anchorCaptureFor(match, '@import.');
449
+ if (anchor === undefined)
450
+ continue;
451
+ const parsed = provider.interpretImport(match);
452
+ if (parsed === null)
453
+ continue;
454
+ parsedImports.push(parsed);
455
+ }
456
+ }
457
+ // ─── Pass 4: collect type bindings ─────────────────────────────────────────
458
+ function pass4CollectTypeBindings(matches, drafts, positionIndex, filePath, provider, scopeTree) {
459
+ const draftById = new Map();
460
+ for (const d of drafts)
461
+ draftById.set(d.id, d);
462
+ for (const match of matches) {
463
+ const anchor = anchorCaptureFor(match, '@type-binding.');
464
+ if (anchor === undefined)
465
+ continue;
466
+ const parsed = provider.interpretTypeBinding?.(match);
467
+ if (parsed === null || parsed === undefined)
468
+ continue;
469
+ const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
470
+ if (innermostId === undefined)
471
+ continue;
472
+ const innermost = draftById.get(innermostId);
473
+ if (innermost === undefined)
474
+ continue;
475
+ // Auto-hoist for scope-creating type bindings (e.g. Python's
476
+ // `@type-binding.return` whose anchor is the function_definition
477
+ // itself). Same condition as Pass 2 — when the anchor coincides
478
+ // with the innermost scope's range, the binding belongs in the
479
+ // enclosing scope (callers, not the function body, look up the
480
+ // return type by the function's name).
481
+ const autoHostedId = innermost.parent !== null && rangesEqual(anchor.range, innermost.range)
482
+ ? innermost.parent
483
+ : innermost.id;
484
+ // `bindingScopeFor` may hoist the type binding to an outer scope.
485
+ const hostId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? autoHostedId;
486
+ const host = draftById.get(hostId) ?? innermost;
487
+ const typeRef = {
488
+ rawName: parsed.rawTypeName,
489
+ declaredAtScope: host.id,
490
+ source: parsed.source,
491
+ };
492
+ // Prefer stronger sources when multiple matches fire for the same
493
+ // bound name in the same scope. Example: `u: User = find()` matches
494
+ // both the annotation and constructor-inferred patterns; the explicit
495
+ // annotation (stronger source) must win over the call-site guess
496
+ // regardless of query-match arrival order.
497
+ const existing = host.typeBindings.get(parsed.boundName);
498
+ if (existing === undefined ||
499
+ typeBindingStrength(typeRef.source) >= typeBindingStrength(existing.source)) {
500
+ host.typeBindings.set(parsed.boundName, typeRef);
501
+ }
502
+ }
503
+ // ── Transitive closure over identifier-chain type bindings ─────────
504
+ // Captures like `(assignment left: (ident) right: (ident))` emit a
505
+ // TypeRef whose `rawName` is the RHS identifier. When the RHS name is
506
+ // itself a bound variable with a known type in the same scope (or a
507
+ // parent scope), follow the chain so `alias` ultimately points at the
508
+ // class type — not at another local variable name. Without this,
509
+ // `resolveTypeRef` hits the chained name, sees it's a local Variable
510
+ // (non-type kind), and strict-returns null.
511
+ for (const draft of drafts) {
512
+ for (const [name, ref] of draft.typeBindings) {
513
+ const resolved = followChainedRef(ref, draftById);
514
+ if (resolved !== ref)
515
+ draft.typeBindings.set(name, resolved);
516
+ }
517
+ }
518
+ }
519
+ /** Max chain depth: practical programs rarely exceed 4-5 re-bindings;
520
+ * the cap just prevents runaway loops when providers emit cycles. */
521
+ const CHAIN_MAX_DEPTH = 16;
522
+ /**
523
+ * Follow an identifier-chain TypeRef through successive typeBindings
524
+ * lookups in the declaring scope and its ancestors. Returns the terminal
525
+ * TypeRef (or the original if the chain dead-ends or cycles).
526
+ */
527
+ function followChainedRef(start, draftById) {
528
+ let current = start;
529
+ const visited = new Set();
530
+ for (let depth = 0; depth < CHAIN_MAX_DEPTH; depth++) {
531
+ // A rawName containing a dot (`models.User`) goes through
532
+ // `QualifiedNameIndex` at resolution time — don't follow it here.
533
+ if (current.rawName.includes('.'))
534
+ return current;
535
+ // Look up the current rawName in the declaring scope and walk up
536
+ // the chain until we hit a scope that has a binding for it.
537
+ let scopeId = current.declaredAtScope;
538
+ let next;
539
+ while (scopeId !== null) {
540
+ const scope = draftById.get(scopeId);
541
+ if (scope === undefined)
542
+ break;
543
+ next = scope.typeBindings.get(current.rawName);
544
+ if (next !== undefined)
545
+ break;
546
+ scopeId = scope.parent;
547
+ }
548
+ if (next === undefined)
549
+ return current; // dead end — nothing to chain to
550
+ if (next === current)
551
+ return current; // self-ref
552
+ if (visited.has(next.rawName))
553
+ return current; // cycle guard
554
+ visited.add(next.rawName);
555
+ current = next;
556
+ }
557
+ return current;
558
+ }
559
+ /**
560
+ * Priority ordering when multiple `TypeRef`s compete for the same bound
561
+ * name in the same scope. Higher number wins; ties keep the later match
562
+ * (last-write-wins preserves historical order within a tier).
563
+ *
564
+ * Rationale: explicit annotations always beat inferred ones because they
565
+ * reflect user intent. `self`/`cls` are treated as strongly as annotations
566
+ * because they are language-required receiver types.
567
+ */
568
+ function typeBindingStrength(source) {
569
+ switch (source) {
570
+ case 'annotation':
571
+ case 'parameter-annotation':
572
+ case 'return-annotation':
573
+ case 'self':
574
+ return 2;
575
+ case 'assignment-inferred':
576
+ case 'constructor-inferred':
577
+ case 'receiver-propagated':
578
+ return 1;
579
+ default:
580
+ return 0;
581
+ }
582
+ }
583
+ // ─── Pass 5: collect reference sites ───────────────────────────────────────
584
+ function pass5CollectReferences(matches, positionIndex, filePath, referenceSites, provider, scopeTree) {
585
+ for (const match of matches) {
586
+ const anchor = anchorCaptureFor(match, '@reference.');
587
+ if (anchor === undefined)
588
+ continue;
589
+ const kind = referenceKindFromAnchor(anchor.name);
590
+ if (kind === undefined)
591
+ continue;
592
+ const nameCap = match['@reference.name'] ?? anchor;
593
+ const inScopeId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
594
+ if (inScopeId === undefined)
595
+ continue;
596
+ const callForm = kind === 'call'
597
+ ? classifyCallFormForMatch(match, anchor.name, provider, scopeTree, inScopeId)
598
+ : undefined;
599
+ const explicitReceiver = extractExplicitReceiver(match);
600
+ const arity = extractArity(match);
601
+ const argumentTypes = extractArgumentTypes(match);
602
+ const site = {
603
+ name: nameCap.text,
604
+ atRange: anchor.range,
605
+ inScope: inScopeId,
606
+ kind,
607
+ ...(callForm !== undefined ? { callForm } : {}),
608
+ ...(explicitReceiver !== undefined ? { explicitReceiver } : {}),
609
+ ...(arity !== undefined ? { arity } : {}),
610
+ ...(argumentTypes !== undefined ? { argumentTypes } : {}),
611
+ };
612
+ referenceSites.push(site);
613
+ }
614
+ }
615
+ function referenceKindFromAnchor(name) {
616
+ const suffix = name.slice('@reference.'.length);
617
+ // Strip sub-tag after the kind (`@reference.call.member` → `call`).
618
+ const firstDot = suffix.indexOf('.');
619
+ const head = firstDot === -1 ? suffix : suffix.slice(0, firstDot);
620
+ switch (head.toLowerCase()) {
621
+ case 'call':
622
+ return 'call';
623
+ case 'read':
624
+ return 'read';
625
+ case 'write':
626
+ return 'write';
627
+ case 'type':
628
+ case 'type_reference':
629
+ return 'type-reference';
630
+ case 'inherits':
631
+ return 'inherits';
632
+ case 'import_use':
633
+ case 'import-use':
634
+ return 'import-use';
635
+ default:
636
+ return undefined;
637
+ }
638
+ }
639
+ function classifyCallFormForMatch(match, anchorName, provider, scopeTree, inScopeId) {
640
+ // Declarative sub-tag path first: `@reference.call.member` → 'member'.
641
+ const suffix = anchorName.slice('@reference.call.'.length);
642
+ switch (suffix.toLowerCase()) {
643
+ case 'free':
644
+ return 'free';
645
+ case 'member':
646
+ return 'member';
647
+ case 'constructor':
648
+ return 'constructor';
649
+ case 'index':
650
+ return 'index';
651
+ }
652
+ // Hook-based path: provider knows.
653
+ const hook = provider.classifyCallForm;
654
+ if (hook !== undefined) {
655
+ const scope = scopeTree.getScope(inScopeId);
656
+ if (scope !== undefined)
657
+ return hook(match, scope);
658
+ }
659
+ return 'free';
660
+ }
661
+ function extractExplicitReceiver(match) {
662
+ const cap = match['@reference.receiver'];
663
+ if (cap === undefined)
664
+ return undefined;
665
+ return { name: cap.text };
666
+ }
667
+ function extractArity(match) {
668
+ const cap = match['@reference.arity'];
669
+ if (cap === undefined)
670
+ return undefined;
671
+ const n = Number.parseInt(cap.text, 10);
672
+ return Number.isFinite(n) ? n : undefined;
673
+ }
674
+ function extractArgumentTypes(match) {
675
+ const cap = match['@reference.parameter-types'];
676
+ if (cap === undefined)
677
+ return undefined;
678
+ try {
679
+ const parsed = JSON.parse(cap.text);
680
+ if (Array.isArray(parsed) && parsed.every((x) => typeof x === 'string'))
681
+ return parsed;
682
+ }
683
+ catch {
684
+ /* malformed — fall through */
685
+ }
686
+ return undefined;
687
+ }
688
+ // ─── Internal: range + capture utilities ───────────────────────────────────
689
+ function rangesEqual(a, b) {
690
+ return (a.startLine === b.startLine &&
691
+ a.startCol === b.startCol &&
692
+ a.endLine === b.endLine &&
693
+ a.endCol === b.endCol);
694
+ }
695
+ function rangeStrictlyContains(outer, inner) {
696
+ if (outer.startLine === inner.startLine &&
697
+ outer.startCol === inner.startCol &&
698
+ outer.endLine === inner.endLine &&
699
+ outer.endCol === inner.endCol) {
700
+ return false;
701
+ }
702
+ const startsBefore = outer.startLine < inner.startLine ||
703
+ (outer.startLine === inner.startLine && outer.startCol <= inner.startCol);
704
+ const endsAfter = outer.endLine > inner.endLine ||
705
+ (outer.endLine === inner.endLine && outer.endCol >= inner.endCol);
706
+ return startsBefore && endsAfter;
707
+ }
708
+ /**
709
+ * Capture names that are never anchors — they are sub-tags nested inside a
710
+ * larger anchor (e.g., the receiver expression inside a `@reference.call`
711
+ * may span more source than the called name, but is not the call itself).
712
+ *
713
+ * The list is maintained here centrally rather than per-pass because the
714
+ * set is small and stable; adding a new sub-tag convention is a one-line
715
+ * change.
716
+ */
717
+ const KNOWN_SUB_TAGS = new Set([
718
+ '@declaration.name',
719
+ '@declaration.qualified_name',
720
+ '@import.name',
721
+ '@import.source',
722
+ '@import.alias',
723
+ '@type-binding.name',
724
+ '@type-binding.type',
725
+ '@reference.name',
726
+ '@reference.receiver',
727
+ '@reference.arity',
728
+ '@reference.parameter-types',
729
+ '@declaration.parameter-count',
730
+ '@declaration.required-parameter-count',
731
+ '@declaration.parameter-types',
732
+ ]);
733
+ /**
734
+ * Return the anchor capture for a match — the one whose name begins with
735
+ * `prefix` AND is not in the known-sub-tag set. When multiple candidates
736
+ * remain, the broadest-ranged one wins: tree-sitter queries often tag
737
+ * both a whole statement and a sub-token under the same topic
738
+ * (`@scope.function` + `@scope.function.name`); the anchor is the
739
+ * statement-level one.
740
+ */
741
+ function anchorCaptureFor(match, prefix) {
742
+ let best;
743
+ let bestSpan = -1;
744
+ for (const name of Object.keys(match)) {
745
+ if (!name.startsWith(prefix))
746
+ continue;
747
+ if (KNOWN_SUB_TAGS.has(name))
748
+ continue;
749
+ const cap = match[name];
750
+ const span = (cap.range.endLine - cap.range.startLine) * 1_000_000 +
751
+ (cap.range.endCol - cap.range.startCol);
752
+ if (span > bestSpan) {
753
+ bestSpan = span;
754
+ best = cap;
755
+ }
756
+ }
757
+ return best;
758
+ }