gitnexus 1.6.3-rc.2 → 1.6.3-rc.21

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 (130) hide show
  1. package/dist/_shared/graph/types.d.ts +16 -0
  2. package/dist/_shared/graph/types.d.ts.map +1 -1
  3. package/dist/_shared/index.d.ts +41 -1
  4. package/dist/_shared/index.d.ts.map +1 -1
  5. package/dist/_shared/index.js +28 -0
  6. package/dist/_shared/index.js.map +1 -1
  7. package/dist/_shared/scope-resolution/def-index.d.ts +36 -0
  8. package/dist/_shared/scope-resolution/def-index.d.ts.map +1 -0
  9. package/dist/_shared/scope-resolution/def-index.js +51 -0
  10. package/dist/_shared/scope-resolution/def-index.js.map +1 -0
  11. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +139 -0
  12. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -0
  13. package/dist/_shared/scope-resolution/finalize-algorithm.js +479 -0
  14. package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -0
  15. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +80 -0
  16. package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -0
  17. package/dist/_shared/scope-resolution/method-dispatch-index.js +79 -0
  18. package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -0
  19. package/dist/_shared/scope-resolution/module-scope-index.d.ts +46 -0
  20. package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -0
  21. package/dist/_shared/scope-resolution/module-scope-index.js +58 -0
  22. package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -0
  23. package/dist/_shared/scope-resolution/parsed-file.d.ts +64 -0
  24. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
  25. package/dist/_shared/scope-resolution/parsed-file.js +42 -0
  26. package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
  27. package/dist/_shared/scope-resolution/position-index.d.ts +62 -0
  28. package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -0
  29. package/dist/_shared/scope-resolution/position-index.js +134 -0
  30. package/dist/_shared/scope-resolution/position-index.js.map +1 -0
  31. package/dist/_shared/scope-resolution/qualified-name-index.d.ts +44 -0
  32. package/dist/_shared/scope-resolution/qualified-name-index.d.ts.map +1 -0
  33. package/dist/_shared/scope-resolution/qualified-name-index.js +75 -0
  34. package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -0
  35. package/dist/_shared/scope-resolution/reference-site.d.ts +67 -0
  36. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
  37. package/dist/_shared/scope-resolution/reference-site.js +24 -0
  38. package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
  39. package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
  40. package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
  41. package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
  42. package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
  43. package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
  44. package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
  45. package/dist/_shared/scope-resolution/registries/context.js +44 -0
  46. package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
  47. package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
  48. package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
  49. package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
  50. package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
  51. package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
  52. package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
  53. package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
  54. package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
  55. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
  56. package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
  57. package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
  58. package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
  59. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
  60. package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
  61. package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
  62. package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
  63. package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
  64. package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
  65. package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
  66. package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
  67. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
  68. package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
  69. package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
  70. package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
  71. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +53 -0
  72. package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -0
  73. package/dist/_shared/scope-resolution/resolve-type-ref.js +126 -0
  74. package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -0
  75. package/dist/_shared/scope-resolution/scope-id.d.ts +43 -0
  76. package/dist/_shared/scope-resolution/scope-id.d.ts.map +1 -0
  77. package/dist/_shared/scope-resolution/scope-id.js +46 -0
  78. package/dist/_shared/scope-resolution/scope-id.js.map +1 -0
  79. package/dist/_shared/scope-resolution/scope-tree.d.ts +61 -0
  80. package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -0
  81. package/dist/_shared/scope-resolution/scope-tree.js +186 -0
  82. package/dist/_shared/scope-resolution/scope-tree.js.map +1 -0
  83. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +63 -0
  84. package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -0
  85. package/dist/_shared/scope-resolution/shadow/aggregate.js +122 -0
  86. package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -0
  87. package/dist/_shared/scope-resolution/shadow/diff.d.ts +59 -0
  88. package/dist/_shared/scope-resolution/shadow/diff.d.ts.map +1 -0
  89. package/dist/_shared/scope-resolution/shadow/diff.js +79 -0
  90. package/dist/_shared/scope-resolution/shadow/diff.js.map +1 -0
  91. package/dist/_shared/scope-resolution/types.d.ts +156 -0
  92. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  93. package/dist/cli/analyze.d.ts +15 -0
  94. package/dist/cli/analyze.js +22 -1
  95. package/dist/cli/index.js +4 -0
  96. package/dist/cli/list.js +11 -1
  97. package/dist/core/ingestion/emit-references.d.ts +88 -0
  98. package/dist/core/ingestion/emit-references.js +229 -0
  99. package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
  100. package/dist/core/ingestion/finalize-orchestrator.js +139 -0
  101. package/dist/core/ingestion/framework-detection.js +6 -2
  102. package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
  103. package/dist/core/ingestion/import-target-adapter.js +95 -0
  104. package/dist/core/ingestion/language-provider.d.ts +187 -1
  105. package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
  106. package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
  107. package/dist/core/ingestion/model/semantic-model.d.ts +25 -0
  108. package/dist/core/ingestion/model/semantic-model.js +16 -0
  109. package/dist/core/ingestion/parsing-processor.d.ts +9 -0
  110. package/dist/core/ingestion/parsing-processor.js +10 -0
  111. package/dist/core/ingestion/registry-primary-flag.d.ts +59 -0
  112. package/dist/core/ingestion/registry-primary-flag.js +78 -0
  113. package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
  114. package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
  115. package/dist/core/ingestion/scope-extractor.d.ts +87 -0
  116. package/dist/core/ingestion/scope-extractor.js +603 -0
  117. package/dist/core/ingestion/shadow-harness.d.ts +113 -0
  118. package/dist/core/ingestion/shadow-harness.js +148 -0
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
  120. package/dist/core/ingestion/workers/parse-worker.js +20 -1
  121. package/dist/core/run-analyze.d.ts +21 -0
  122. package/dist/core/run-analyze.js +15 -4
  123. package/dist/core/search/phase-timer.d.ts +72 -0
  124. package/dist/core/search/phase-timer.js +106 -0
  125. package/dist/mcp/local/local-backend.js +70 -8
  126. package/dist/storage/git.d.ts +25 -0
  127. package/dist/storage/git.js +52 -0
  128. package/dist/storage/repo-manager.d.ts +70 -1
  129. package/dist/storage/repo-manager.js +107 -5
  130. package/package.json +1 -1
@@ -0,0 +1,603 @@
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.shouldCreateScope` (default true) and
29
+ * `provider.resolveScopeKind` (default: suffix of the capture name).
30
+ * Derive parent by lexical-range containment. Hand the resulting
31
+ * `Scope[]` to `buildScopeTree` for validation.
32
+ * 2. **Attach declarations + local bindings.** Walk `@declaration.*`
33
+ * matches. For each, build a `SymbolDefinition` and attach it to
34
+ * `provider.bindingScopeFor` (default: innermost containing scope)
35
+ * as `ownedDefs` + a local `BindingRef { origin: 'local' }`.
36
+ * 3. **Collect raw imports.** Walk `@import.*` matches. Call
37
+ * `provider.interpretImport` per match; attach the returned
38
+ * `ParsedImport` to the ParsedFile (not to any `Scope` — finalize
39
+ * reconstructs the owning scope via `provider.importOwningScope`
40
+ * during Phase 2).
41
+ * 4. **Collect type bindings.** Walk `@type-binding.*` matches. Call
42
+ * `provider.interpretTypeBinding` per match. Attach the resulting
43
+ * `TypeRef` to the innermost containing scope's `typeBindings`
44
+ * (or override via `provider.bindingScopeFor` if set).
45
+ * 5. **Collect reference sites.** Walk `@reference.*` matches. Emit
46
+ * one `ReferenceSite` per match. Classify call form via
47
+ * `provider.classifyCallForm` (default: the capture's sub-tag if
48
+ * present; else `'free'`).
49
+ *
50
+ * ## What gets attached where
51
+ *
52
+ * - `Scope.bindings` — **local bindings only** at this stage (Pass 2).
53
+ * Finalize (#915) merges imports/wildcards on top.
54
+ * - `Scope.ownedDefs` — declarations structurally owned by this scope.
55
+ * - `Scope.typeBindings` — local type facts (parameter annotations, `self`).
56
+ * - `Scope.imports` — empty here. Populated by the finalize algorithm
57
+ * when it resolves `ParsedImport.targetRaw`.
58
+ * - `ParsedFile.parsedImports` — every raw import in this file.
59
+ * - `ParsedFile.localDefs` — flattened union of `Scope.ownedDefs`.
60
+ * - `ParsedFile.referenceSites` — pre-resolution usage facts.
61
+ */
62
+ import { buildPositionIndex, buildScopeTree, makeScopeId } from '../../_shared/index.js';
63
+ // ─── Public entry point ─────────────────────────────────────────────────────
64
+ /**
65
+ * Drive the five extraction passes and return a `ParsedFile`.
66
+ *
67
+ * Throws `ScopeTreeInvariantError` (from #912) when the provider emits
68
+ * captures that violate structural scope invariants. The error surfaces
69
+ * upward rather than being silently corrected — a malformed capture set
70
+ * is a bug in the provider's `emitScopeCaptures`, not a data condition
71
+ * to tolerate.
72
+ */
73
+ export function extract(matches, filePath, provider) {
74
+ // Partition matches by topic up front — one linear pass over the input.
75
+ const partitioned = partitionByTopic(matches);
76
+ // ── Pass 1: build the scope tree ─────────────────────────────────────
77
+ const scopeDrafts = pass1BuildScopes(partitioned.scope, filePath, provider);
78
+ const scopes = scopeDrafts.map(draftToScope);
79
+ // buildScopeTree validates invariants (throws on violation) and exposes
80
+ // the lookup contract consumed by Passes 2-5.
81
+ //
82
+ // **Snapshot semantics.** Both `scopeTree` and `positionIndex` are built
83
+ // from the post-Pass-1 `scopes` — parent/range/kind are accurate, but
84
+ // `bindings`, `ownedDefs`, and `typeBindings` are all empty here. Later
85
+ // passes write into the *drafts*, not into these snapshots; any hook
86
+ // that reads `scope.bindings` etc. via the `scopeTree` argument sees a
87
+ // structural view only. This is by design — hooks use scopeTree for
88
+ // "what's the parent chain?" queries, not for content queries.
89
+ const scopeTree = buildScopeTree(scopes);
90
+ const positionIndex = buildPositionIndex(scopes);
91
+ const moduleScope = scopeDrafts.find((s) => s.kind === 'Module');
92
+ if (moduleScope === undefined) {
93
+ throw new Error(`ScopeExtractor: no Module scope found for '${filePath}'. ` +
94
+ `Provider must emit at least one @scope.module capture per file.`);
95
+ }
96
+ // ── Pass 2: attach declarations + local bindings ────────────────────
97
+ const localDefs = [];
98
+ pass2AttachDeclarations(partitioned.declaration, scopeDrafts, positionIndex, localDefs, filePath, provider, scopeTree);
99
+ // ── Pass 3: collect raw imports ─────────────────────────────────────
100
+ const parsedImports = [];
101
+ pass3CollectImports(partitioned.import_, parsedImports, provider);
102
+ // ── Pass 4: collect type bindings ───────────────────────────────────
103
+ pass4CollectTypeBindings(partitioned.typeBinding, scopeDrafts, positionIndex, filePath, provider, scopeTree);
104
+ // ── Pass 5: collect reference sites ─────────────────────────────────
105
+ const referenceSites = [];
106
+ pass5CollectReferences(partitioned.reference, positionIndex, filePath, referenceSites, provider, scopeTree);
107
+ // Freeze Scope drafts into final shape and return.
108
+ const frozenScopes = scopeDrafts.map(draftToScope);
109
+ return Object.freeze({
110
+ filePath,
111
+ moduleScope: moduleScope.id,
112
+ scopes: Object.freeze(frozenScopes),
113
+ parsedImports: Object.freeze(parsedImports.slice()),
114
+ localDefs: Object.freeze(localDefs.slice()),
115
+ referenceSites: Object.freeze(referenceSites.slice()),
116
+ });
117
+ }
118
+ /**
119
+ * Bucket each match by the topic of its anchor capture. The anchor is the
120
+ * capture whose name is prefixed with the match's topic (`@scope.*`,
121
+ * `@declaration.*`, `@import.*`, `@type-binding.*`, `@reference.*`).
122
+ *
123
+ * A match may contain additional captures (e.g., `@import.source`,
124
+ * `@declaration.class.name`) that are used by the provider hooks to
125
+ * decode details. Those live inside the `CaptureMatch` and are surfaced
126
+ * to hooks verbatim — the extractor itself only routes by anchor.
127
+ */
128
+ function partitionByTopic(matches) {
129
+ const scope = [];
130
+ const declaration = [];
131
+ const import_ = [];
132
+ const typeBinding = [];
133
+ const reference = [];
134
+ for (const match of matches) {
135
+ const topic = topicOf(match);
136
+ switch (topic) {
137
+ case 'scope':
138
+ scope.push(match);
139
+ break;
140
+ case 'declaration':
141
+ declaration.push(match);
142
+ break;
143
+ case 'import':
144
+ import_.push(match);
145
+ break;
146
+ case 'type-binding':
147
+ typeBinding.push(match);
148
+ break;
149
+ case 'reference':
150
+ reference.push(match);
151
+ break;
152
+ case 'unknown':
153
+ // Unrecognized anchor — silently skip. Providers may emit extra
154
+ // captures (e.g., `@comment`) that the extractor has no topic for.
155
+ break;
156
+ }
157
+ }
158
+ return { scope, declaration, import_, typeBinding, reference };
159
+ }
160
+ function topicOf(match) {
161
+ // The anchor is the capture whose name uses one of the known topic
162
+ // prefixes. For multi-capture matches, ALL captures share the topic;
163
+ // we pick the first matching key for efficiency.
164
+ for (const name of Object.keys(match)) {
165
+ if (name.startsWith('@scope.'))
166
+ return 'scope';
167
+ if (name.startsWith('@declaration.'))
168
+ return 'declaration';
169
+ if (name.startsWith('@import.'))
170
+ return 'import';
171
+ if (name.startsWith('@type-binding.'))
172
+ return 'type-binding';
173
+ if (name.startsWith('@reference.'))
174
+ return 'reference';
175
+ }
176
+ return 'unknown';
177
+ }
178
+ function draftToScope(draft) {
179
+ const frozenBindings = new Map();
180
+ for (const [name, refs] of draft.bindings) {
181
+ frozenBindings.set(name, Object.freeze(refs.slice()));
182
+ }
183
+ return {
184
+ id: draft.id,
185
+ parent: draft.parent,
186
+ kind: draft.kind,
187
+ range: draft.range,
188
+ filePath: draft.filePath,
189
+ bindings: frozenBindings,
190
+ ownedDefs: Object.freeze(draft.ownedDefs.slice()),
191
+ imports: Object.freeze(draft.imports.slice()),
192
+ typeBindings: new Map(draft.typeBindings),
193
+ };
194
+ }
195
+ // ─── Pass 1: build scope tree ──────────────────────────────────────────────
196
+ /**
197
+ * Convert `@scope.*` matches into `ScopeDraft[]`. Parent relationships
198
+ * are derived from range containment (outermost scope containing `range`
199
+ * becomes the parent). Scopes with `shouldCreateScope === false` are
200
+ * silently omitted — their children reparent to the next enclosing
201
+ * real scope.
202
+ */
203
+ function pass1BuildScopes(matches, filePath, provider) {
204
+ const candidates = [];
205
+ for (const match of matches) {
206
+ const anchor = anchorCaptureFor(match, '@scope.');
207
+ if (anchor === undefined)
208
+ continue;
209
+ const kind = resolveKindForScopeMatch(match, anchor, provider);
210
+ if (kind === null)
211
+ continue;
212
+ const create = provider.shouldCreateScope?.(match) ?? true;
213
+ const id = makeScopeId({ filePath, range: anchor.range, kind });
214
+ candidates.push({ match, range: anchor.range, kind, create, id });
215
+ }
216
+ // Sort by (startLine, startCol) ASC, (endLine, endCol) DESC so outer
217
+ // scopes appear before their children for parent-resolution.
218
+ candidates.sort((a, b) => {
219
+ if (a.range.startLine !== b.range.startLine)
220
+ return a.range.startLine - b.range.startLine;
221
+ if (a.range.startCol !== b.range.startCol)
222
+ return a.range.startCol - b.range.startCol;
223
+ if (a.range.endLine !== b.range.endLine)
224
+ return b.range.endLine - a.range.endLine;
225
+ return b.range.endCol - a.range.endCol;
226
+ });
227
+ const drafts = [];
228
+ const stack = []; // enclosing real scopes, outermost at [0]
229
+ for (const cand of candidates) {
230
+ // Pop the stack until the top strictly contains this candidate.
231
+ while (stack.length > 0 && !rangeStrictlyContains(stack[stack.length - 1].range, cand.range)) {
232
+ stack.pop();
233
+ }
234
+ if (cand.create) {
235
+ const parent = stack.length > 0 ? stack[stack.length - 1].id : null;
236
+ drafts.push(makeDraft(cand.id, parent, cand.kind, cand.range, filePath));
237
+ stack.push(cand);
238
+ }
239
+ // If `cand.create === false`, we don't push it onto the stack — child
240
+ // scopes will reparent to whatever's below it.
241
+ }
242
+ return drafts;
243
+ }
244
+ function resolveKindForScopeMatch(match, anchor, provider) {
245
+ // Provider override takes precedence.
246
+ const override = provider.resolveScopeKind?.(match);
247
+ if (override !== undefined && override !== null)
248
+ return override;
249
+ // Default: derive from capture name suffix (`@scope.function` → 'Function').
250
+ const suffix = anchor.name.slice('@scope.'.length);
251
+ switch (suffix.toLowerCase()) {
252
+ case 'module':
253
+ return 'Module';
254
+ case 'namespace':
255
+ return 'Namespace';
256
+ case 'class':
257
+ return 'Class';
258
+ case 'function':
259
+ return 'Function';
260
+ case 'block':
261
+ return 'Block';
262
+ case 'expression':
263
+ return 'Expression';
264
+ default:
265
+ return null;
266
+ }
267
+ }
268
+ function makeDraft(id, parent, kind, range, filePath) {
269
+ return {
270
+ id,
271
+ parent,
272
+ kind,
273
+ range,
274
+ filePath,
275
+ bindings: new Map(),
276
+ ownedDefs: [],
277
+ imports: [],
278
+ typeBindings: new Map(),
279
+ };
280
+ }
281
+ // ─── Pass 2: attach declarations + local bindings ──────────────────────────
282
+ function pass2AttachDeclarations(matches, drafts, positionIndex, localDefs, filePath, provider, scopeTree) {
283
+ const draftById = new Map();
284
+ for (const d of drafts)
285
+ draftById.set(d.id, d);
286
+ for (const match of matches) {
287
+ const anchor = anchorCaptureFor(match, '@declaration.');
288
+ if (anchor === undefined)
289
+ continue;
290
+ const def = buildDefFromDeclarationMatch(match, anchor, filePath);
291
+ if (def === undefined)
292
+ continue;
293
+ // Find the innermost scope that contains the declaration's anchor range.
294
+ const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
295
+ if (innermostId === undefined)
296
+ continue;
297
+ const innermost = draftById.get(innermostId);
298
+ if (innermost === undefined)
299
+ continue;
300
+ // Ownership: attach the def to the innermost scope's `ownedDefs` — that
301
+ // is the structural owner. `def.ownerId` is NOT populated here — the
302
+ // extractor has no clean path to the parent's own DefId mid-extraction
303
+ // (the parent declaration may not yet have been processed, or may live
304
+ // in a different scope entirely). Providers that need `ownerId` should
305
+ // set it directly from the declaration hook (e.g., derive from the
306
+ // `@declaration.owner` capture or the parent scope id); otherwise
307
+ // `finalize` populates method/field `ownerId` via `MethodDispatchIndex`
308
+ // (#914) in a follow-up pass that sees every def already in place.
309
+ innermost.ownedDefs.push(def);
310
+ localDefs.push(def);
311
+ // Binding visibility: default to innermost; allow hoisting via
312
+ // `provider.bindingScopeFor`. `draftToScope(innermost)` here is a
313
+ // **structural** snapshot — parent/range/kind only. Hooks MUST NOT
314
+ // rely on `scope.bindings`, `ownedDefs`, or `typeBindings` being
315
+ // populated during Pass 2: those fields are written across passes,
316
+ // so reading them mid-extraction yields a partial view. The
317
+ // `scopeTree` argument is similarly snapshot-before-mutation.
318
+ const bindingScopeId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? innermost.id;
319
+ const bindingHost = draftById.get(bindingScopeId) ?? innermost;
320
+ const nameKey = deriveDeclarationName(match, def);
321
+ if (nameKey === undefined)
322
+ continue;
323
+ const existing = bindingHost.bindings.get(nameKey) ?? [];
324
+ existing.push({ def, origin: 'local' });
325
+ bindingHost.bindings.set(nameKey, existing);
326
+ }
327
+ }
328
+ function buildDefFromDeclarationMatch(match, anchor, filePath) {
329
+ // Anchor name pattern: `@declaration.<kind>` where <kind> maps to NodeLabel.
330
+ const kindStr = anchor.name.slice('@declaration.'.length);
331
+ const type = normalizeNodeLabel(kindStr);
332
+ if (type === undefined)
333
+ return undefined;
334
+ const nameCap = match['@declaration.name'] ?? match[`@declaration.${kindStr}.name`] ?? match[anchor.name];
335
+ if (nameCap === undefined)
336
+ return undefined;
337
+ const qualifiedCap = match['@declaration.qualified_name'];
338
+ const qualifiedName = qualifiedCap?.text;
339
+ return {
340
+ nodeId: makeDefId(filePath, anchor.range, type, nameCap.text),
341
+ filePath,
342
+ type,
343
+ ...(qualifiedName !== undefined ? { qualifiedName } : { qualifiedName: nameCap.text }),
344
+ };
345
+ }
346
+ function deriveDeclarationName(match, def) {
347
+ const nameCap = match['@declaration.name'] ??
348
+ match[Object.keys(match).find((k) => k.startsWith('@declaration.') && k.endsWith('.name')) ?? ''];
349
+ if (nameCap !== undefined)
350
+ return nameCap.text;
351
+ // Fall back to qualifiedName tail.
352
+ const q = def.qualifiedName;
353
+ if (q !== undefined && q.length > 0) {
354
+ const dot = q.lastIndexOf('.');
355
+ return dot === -1 ? q : q.slice(dot + 1);
356
+ }
357
+ return undefined;
358
+ }
359
+ /**
360
+ * Map a lower-case declaration kind (from `@declaration.<kind>`) to a
361
+ * graph `NodeLabel`. Silently returns `undefined` for kinds we don't
362
+ * recognize — providers can emit richer captures without breaking the
363
+ * driver.
364
+ */
365
+ function normalizeNodeLabel(kindStr) {
366
+ switch (kindStr.toLowerCase()) {
367
+ case 'class':
368
+ return 'Class';
369
+ case 'interface':
370
+ return 'Interface';
371
+ case 'enum':
372
+ return 'Enum';
373
+ case 'struct':
374
+ return 'Struct';
375
+ case 'union':
376
+ return 'Union';
377
+ case 'trait':
378
+ return 'Trait';
379
+ case 'method':
380
+ return 'Method';
381
+ case 'function':
382
+ return 'Function';
383
+ case 'constructor':
384
+ return 'Constructor';
385
+ case 'field':
386
+ case 'property':
387
+ return 'Property';
388
+ case 'variable':
389
+ case 'const':
390
+ return 'Variable';
391
+ case 'typealias':
392
+ case 'type_alias':
393
+ return 'TypeAlias';
394
+ case 'typedef':
395
+ return 'Typedef';
396
+ case 'record':
397
+ return 'Record';
398
+ case 'delegate':
399
+ return 'Delegate';
400
+ case 'annotation':
401
+ return 'Annotation';
402
+ case 'namespace':
403
+ return 'Namespace';
404
+ default:
405
+ return undefined;
406
+ }
407
+ }
408
+ function makeDefId(filePath, range, type, name) {
409
+ return `def:${filePath}#${range.startLine}:${range.startCol}:${type}:${name}`;
410
+ }
411
+ // ─── Pass 3: collect raw imports ───────────────────────────────────────────
412
+ function pass3CollectImports(matches, parsedImports, provider) {
413
+ if (provider.interpretImport === undefined)
414
+ return;
415
+ for (const match of matches) {
416
+ const anchor = anchorCaptureFor(match, '@import.');
417
+ if (anchor === undefined)
418
+ continue;
419
+ const parsed = provider.interpretImport(match);
420
+ if (parsed === null)
421
+ continue;
422
+ parsedImports.push(parsed);
423
+ }
424
+ }
425
+ // ─── Pass 4: collect type bindings ─────────────────────────────────────────
426
+ function pass4CollectTypeBindings(matches, drafts, positionIndex, filePath, provider, scopeTree) {
427
+ const draftById = new Map();
428
+ for (const d of drafts)
429
+ draftById.set(d.id, d);
430
+ for (const match of matches) {
431
+ const anchor = anchorCaptureFor(match, '@type-binding.');
432
+ if (anchor === undefined)
433
+ continue;
434
+ const parsed = provider.interpretTypeBinding?.(match);
435
+ if (parsed === null || parsed === undefined)
436
+ continue;
437
+ const innermostId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
438
+ if (innermostId === undefined)
439
+ continue;
440
+ const innermost = draftById.get(innermostId);
441
+ if (innermost === undefined)
442
+ continue;
443
+ // `bindingScopeFor` may hoist the type binding to an outer scope.
444
+ const hostId = provider.bindingScopeFor?.(match, draftToScope(innermost), scopeTree) ?? innermost.id;
445
+ const host = draftById.get(hostId) ?? innermost;
446
+ const typeRef = {
447
+ rawName: parsed.rawTypeName,
448
+ declaredAtScope: host.id,
449
+ source: parsed.source,
450
+ };
451
+ host.typeBindings.set(parsed.boundName, typeRef);
452
+ }
453
+ }
454
+ // ─── Pass 5: collect reference sites ───────────────────────────────────────
455
+ function pass5CollectReferences(matches, positionIndex, filePath, referenceSites, provider, scopeTree) {
456
+ for (const match of matches) {
457
+ const anchor = anchorCaptureFor(match, '@reference.');
458
+ if (anchor === undefined)
459
+ continue;
460
+ const kind = referenceKindFromAnchor(anchor.name);
461
+ if (kind === undefined)
462
+ continue;
463
+ const nameCap = match['@reference.name'] ?? anchor;
464
+ const inScopeId = positionIndex.atPosition(filePath, anchor.range.startLine, anchor.range.startCol);
465
+ if (inScopeId === undefined)
466
+ continue;
467
+ const callForm = kind === 'call'
468
+ ? classifyCallFormForMatch(match, anchor.name, provider, scopeTree, inScopeId)
469
+ : undefined;
470
+ const explicitReceiver = extractExplicitReceiver(match);
471
+ const arity = extractArity(match);
472
+ const site = {
473
+ name: nameCap.text,
474
+ atRange: anchor.range,
475
+ inScope: inScopeId,
476
+ kind,
477
+ ...(callForm !== undefined ? { callForm } : {}),
478
+ ...(explicitReceiver !== undefined ? { explicitReceiver } : {}),
479
+ ...(arity !== undefined ? { arity } : {}),
480
+ };
481
+ referenceSites.push(site);
482
+ }
483
+ }
484
+ function referenceKindFromAnchor(name) {
485
+ const suffix = name.slice('@reference.'.length);
486
+ // Strip sub-tag after the kind (`@reference.call.member` → `call`).
487
+ const firstDot = suffix.indexOf('.');
488
+ const head = firstDot === -1 ? suffix : suffix.slice(0, firstDot);
489
+ switch (head.toLowerCase()) {
490
+ case 'call':
491
+ return 'call';
492
+ case 'read':
493
+ return 'read';
494
+ case 'write':
495
+ return 'write';
496
+ case 'type':
497
+ case 'type_reference':
498
+ return 'type-reference';
499
+ case 'inherits':
500
+ return 'inherits';
501
+ case 'import_use':
502
+ case 'import-use':
503
+ return 'import-use';
504
+ default:
505
+ return undefined;
506
+ }
507
+ }
508
+ function classifyCallFormForMatch(match, anchorName, provider, scopeTree, inScopeId) {
509
+ // Declarative sub-tag path first: `@reference.call.member` → 'member'.
510
+ const suffix = anchorName.slice('@reference.call.'.length);
511
+ switch (suffix.toLowerCase()) {
512
+ case 'free':
513
+ return 'free';
514
+ case 'member':
515
+ return 'member';
516
+ case 'constructor':
517
+ return 'constructor';
518
+ case 'index':
519
+ return 'index';
520
+ }
521
+ // Hook-based path: provider knows.
522
+ const hook = provider.classifyCallForm;
523
+ if (hook !== undefined) {
524
+ const scope = scopeTree.getScope(inScopeId);
525
+ if (scope !== undefined)
526
+ return hook(match, scope);
527
+ }
528
+ return 'free';
529
+ }
530
+ function extractExplicitReceiver(match) {
531
+ const cap = match['@reference.receiver'];
532
+ if (cap === undefined)
533
+ return undefined;
534
+ return { name: cap.text };
535
+ }
536
+ function extractArity(match) {
537
+ const cap = match['@reference.arity'];
538
+ if (cap === undefined)
539
+ return undefined;
540
+ const n = Number.parseInt(cap.text, 10);
541
+ return Number.isFinite(n) ? n : undefined;
542
+ }
543
+ // ─── Internal: range + capture utilities ───────────────────────────────────
544
+ function rangeStrictlyContains(outer, inner) {
545
+ if (outer.startLine === inner.startLine &&
546
+ outer.startCol === inner.startCol &&
547
+ outer.endLine === inner.endLine &&
548
+ outer.endCol === inner.endCol) {
549
+ return false;
550
+ }
551
+ const startsBefore = outer.startLine < inner.startLine ||
552
+ (outer.startLine === inner.startLine && outer.startCol <= inner.startCol);
553
+ const endsAfter = outer.endLine > inner.endLine ||
554
+ (outer.endLine === inner.endLine && outer.endCol >= inner.endCol);
555
+ return startsBefore && endsAfter;
556
+ }
557
+ /**
558
+ * Capture names that are never anchors — they are sub-tags nested inside a
559
+ * larger anchor (e.g., the receiver expression inside a `@reference.call`
560
+ * may span more source than the called name, but is not the call itself).
561
+ *
562
+ * The list is maintained here centrally rather than per-pass because the
563
+ * set is small and stable; adding a new sub-tag convention is a one-line
564
+ * change.
565
+ */
566
+ const KNOWN_SUB_TAGS = new Set([
567
+ '@declaration.name',
568
+ '@declaration.qualified_name',
569
+ '@import.name',
570
+ '@import.source',
571
+ '@import.alias',
572
+ '@type-binding.name',
573
+ '@type-binding.type',
574
+ '@reference.name',
575
+ '@reference.receiver',
576
+ '@reference.arity',
577
+ ]);
578
+ /**
579
+ * Return the anchor capture for a match — the one whose name begins with
580
+ * `prefix` AND is not in the known-sub-tag set. When multiple candidates
581
+ * remain, the broadest-ranged one wins: tree-sitter queries often tag
582
+ * both a whole statement and a sub-token under the same topic
583
+ * (`@scope.function` + `@scope.function.name`); the anchor is the
584
+ * statement-level one.
585
+ */
586
+ function anchorCaptureFor(match, prefix) {
587
+ let best;
588
+ let bestSpan = -1;
589
+ for (const name of Object.keys(match)) {
590
+ if (!name.startsWith(prefix))
591
+ continue;
592
+ if (KNOWN_SUB_TAGS.has(name))
593
+ continue;
594
+ const cap = match[name];
595
+ const span = (cap.range.endLine - cap.range.startLine) * 1_000_000 +
596
+ (cap.range.endCol - cap.range.startCol);
597
+ if (span > bestSpan) {
598
+ bestSpan = span;
599
+ best = cap;
600
+ }
601
+ }
602
+ return best;
603
+ }