gitnexus 1.6.6-rc.143 → 1.6.6-rc.145

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.
@@ -161,7 +161,101 @@ export function emitJavaScopeCaptures(sourceText, _filePath, cachedTree) {
161
161
  }
162
162
  out.push(grouped);
163
163
  }
164
- return [...resolveVarTypeBindings(out), ...synthesizeJavaInheritanceReferences(tree.rootNode)];
164
+ return [
165
+ ...resolveVarTypeBindings(out),
166
+ ...synthesizeJavaInheritanceReferences(tree.rootNode),
167
+ ...synthesizeJavaExplicitConstructorReferences(tree.rootNode),
168
+ ];
169
+ }
170
+ /**
171
+ * Synthesize `@reference.call.constructor` captures for explicit constructor
172
+ * invocations — `super(...)` and `this(...)` (F38 #1928). tree-sitter-java
173
+ * models these as `explicit_constructor_invocation` nodes, which the scope
174
+ * query does not match, so the chained-constructor CALLS edges (subclass ctor →
175
+ * superclass ctor; ctor → sibling overload) were silently dropped.
176
+ *
177
+ * The grammar gives no constructor *name* at the call site (the child is a bare
178
+ * `(super)` / `(this)` token), so the target name is resolved structurally:
179
+ * - `this(...)` → the enclosing type's own simple name (constructor symbols
180
+ * are keyed by the declaring class name).
181
+ * - `super(...)` → the enclosing class's superclass simple-name tail (reusing
182
+ * `javaBaseLookupNameNode` so qualified/generic supers reduce
183
+ * to the bare class name, matching the EXTENDS synth). An
184
+ * implicit `Object` super (no `superclass` field) has no
185
+ * in-graph symbol, so it is skipped rather than emitting a
186
+ * dangling reference.
187
+ * Arity is attached so overloaded constructors disambiguate downstream, mirroring
188
+ * the call-site arity synthesized for `new X(...)`.
189
+ */
190
+ function synthesizeJavaExplicitConstructorReferences(root) {
191
+ const out = [];
192
+ const stack = [root];
193
+ while (stack.length > 0) {
194
+ const node = stack.pop();
195
+ if (node.type === 'explicit_constructor_invocation') {
196
+ emitJavaExplicitConstructorRef(out, node);
197
+ }
198
+ for (let i = 0; i < node.namedChildCount; i++) {
199
+ const child = node.namedChild(i);
200
+ if (child !== null)
201
+ stack.push(child);
202
+ }
203
+ }
204
+ return out;
205
+ }
206
+ const TYPE_DECL_NODE_TYPES = new Set([
207
+ 'class_declaration',
208
+ 'enum_declaration',
209
+ 'record_declaration',
210
+ ]);
211
+ function emitJavaExplicitConstructorRef(out, node) {
212
+ const ctor = node.childForFieldName('constructor');
213
+ if (ctor === null)
214
+ return;
215
+ const enclosingType = findEnclosingTypeDeclaration(node);
216
+ if (enclosingType === null)
217
+ return;
218
+ let targetNameNode = null;
219
+ if (ctor.type === 'this') {
220
+ targetNameNode = enclosingType.childForFieldName('name');
221
+ }
222
+ else if (ctor.type === 'super') {
223
+ // Only class_declaration carries a `superclass` field; enum/record cannot
224
+ // declare an explicit superclass, so `super(...)` there has no resolvable
225
+ // target symbol.
226
+ const superclass = enclosingType.childForFieldName('superclass');
227
+ if (superclass === null)
228
+ return;
229
+ for (const base of superclass.namedChildren) {
230
+ if (base === null)
231
+ continue;
232
+ const nameNode = javaBaseLookupNameNode(base);
233
+ if (nameNode !== null) {
234
+ targetNameNode = nameNode;
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ if (targetNameNode === null)
240
+ return;
241
+ const argList = node.childForFieldName('arguments');
242
+ const args = argList === null
243
+ ? []
244
+ : argList.namedChildren.filter((c) => c !== null && c.type !== 'block_comment' && c.type !== 'line_comment');
245
+ out.push({
246
+ '@reference.call.constructor': nodeToCapture('@reference.call.constructor', node),
247
+ '@reference.name': nodeToCapture('@reference.name', targetNameNode),
248
+ '@reference.arity': syntheticCapture('@reference.arity', node, String(args.length)),
249
+ });
250
+ }
251
+ function findEnclosingTypeDeclaration(node) {
252
+ let cur = node.parent;
253
+ while (cur !== null) {
254
+ if (TYPE_DECL_NODE_TYPES.has(cur.type))
255
+ return cur;
256
+ cur = cur.parent;
257
+ }
258
+ return null;
165
259
  }
166
260
  /**
167
261
  * Synthesize `@reference.inherits` captures from Java class heritage so the
@@ -67,10 +67,14 @@ export function interpretJavaTypeBinding(captures) {
67
67
  const typeCap = captures['@type-binding.type'];
68
68
  if (nameCap === undefined || typeCap === undefined)
69
69
  return null;
70
- // Strip qualifier first so that `com.example.BaseModel<T>` becomes
71
- // `BaseModel<T>` before stripGeneric — the JVM-erasure fallback pattern
72
- // requires an unqualified identifier at the start of the string.
73
- const rawType = stripGeneric(stripQualifier(typeCap.text.trim()));
70
+ // Strip generics BEFORE the qualifier (F41 #1928). Stripping the qualifier
71
+ // first uses `lastIndexOf('.')`, which for a qualified *type argument*
72
+ // (`Map<String, com.example.User>`) cuts inside the generic and yields a
73
+ // corrupted `User>`. Unwrapping generics first reduces the string to a single
74
+ // (possibly qualified) class name, then the qualifier strip leaves the bare
75
+ // simple name. `stripGeneric`'s erasure fallback is qualifier-tolerant so a
76
+ // qualified generic base (`com.example.BaseModel<T>`) still reduces correctly.
77
+ const rawType = stripQualifier(stripGeneric(typeCap.text.trim()));
74
78
  // Skip `var` — tree-sitter-java parses `var` as type_identifier with
75
79
  // text "var". When used without a constructor initializer, there's no
76
80
  // concrete type to bind.
@@ -120,8 +124,10 @@ function stripGeneric(text) {
120
124
  // `BaseModel<T>` → `BaseModel`, `Builder<Self>` → `Builder`.
121
125
  // This mirrors JVM type erasure — the raw class name is the resolvable symbol.
122
126
  // The pattern matches up to the first `<` to handle nested generics safely
123
- // (e.g. `BaseModel<List<String>>` → `BaseModel`).
124
- const fallback = text.match(/^([A-Za-z_$][A-Za-z0-9_$]*)<.+>$/s);
127
+ // (e.g. `BaseModel<List<String>>` → `BaseModel`). The base is allowed to be
128
+ // qualified (`com.example.BaseModel<T>` → `com.example.BaseModel`) since the
129
+ // caller strips the qualifier afterwards (F41 #1928).
130
+ const fallback = text.match(/^((?:[A-Za-z_$][A-Za-z0-9_$]*\.)*[A-Za-z_$][A-Za-z0-9_$]*)<.+>$/s);
125
131
  if (fallback !== null)
126
132
  return fallback[1].trim();
127
133
  return text;
@@ -212,8 +212,23 @@ const JAVA_SCOPE_QUERY = `
212
212
  type: (generic_type
213
213
  (type_identifier) @reference.name)) @reference.call.constructor
214
214
 
215
+ ;; References — qualified constructor calls: new pkg.Foo(), new a.b.Foo() (F35 #1928)
216
+ ;; tree-sitter-java parses \`pkg.Foo\` as a scoped_type_identifier whose final
217
+ ;; child is the simple type. Bind that tail as @reference.name (trailing \`.\`
218
+ ;; anchor = last child) so resolution targets \`Foo\`, not the raw \`pkg.Foo\` text.
219
+ ;; Mirrors the TS/JS new-expression qualified-constructor capture.
215
220
  (object_creation_expression
216
- type: (scoped_type_identifier) @reference.call.constructor.qualified) @reference.call.constructor
221
+ type: (scoped_type_identifier
222
+ (type_identifier) @reference.name .) @reference.call.constructor.qualified) @reference.call.constructor
223
+
224
+ ;; References — qualified + generic constructor calls: new pkg.Box<T>() (F35 #1928)
225
+ ;; The base is a generic_type whose first child is a scoped_type_identifier, so
226
+ ;; neither the simple-generic nor the plain-scoped arm above matches it. Bind the
227
+ ;; scoped tail as @reference.name.
228
+ (object_creation_expression
229
+ type: (generic_type
230
+ (scoped_type_identifier
231
+ (type_identifier) @reference.name .) @reference.call.constructor.qualified)) @reference.call.constructor
217
232
 
218
233
  ;; References — method references: User::getName, obj::method
219
234
  (method_reference
@@ -48,6 +48,15 @@ function isCallerAnchorLabel(label) {
48
48
  label === 'Struct' ||
49
49
  label === 'Enum');
50
50
  }
51
+ /**
52
+ * Callables whose same-name overloads occupy distinct graph nodes keyed by
53
+ * parameter types / shape. Must mirror `isOverloadableCallable` in
54
+ * `node-lookup.ts` so registration and lookup agree (Constructor included —
55
+ * #1928 F38).
56
+ */
57
+ function isOverloadableCallable(label) {
58
+ return label === 'Function' || label === 'Method' || label === 'Constructor';
59
+ }
51
60
  /**
52
61
  * Look up a `SymbolDefinition` in the graph node lookup.
53
62
  *
@@ -83,7 +92,7 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
83
92
  if (cHit !== undefined)
84
93
  return cHit;
85
94
  }
86
- if ((def.type === 'Function' || def.type === 'Method') &&
95
+ if (isOverloadableCallable(def.type) &&
87
96
  def.parameterTypes !== undefined &&
88
97
  def.parameterTypeClasses !== undefined) {
89
98
  const shapeTag = parameterShapeIdTag(def.parameterTypes, def.parameterTypeClasses);
@@ -96,8 +105,11 @@ export function resolveDefGraphId(filePath, def, nodeLookup) {
96
105
  }
97
106
  // Overload disambiguation: when the def carries parameter types,
98
107
  // try the parameter-typed key first so same-name same-arity
99
- // overloads route to their distinct graph nodes.
100
- if ((def.type === 'Function' || def.type === 'Method') &&
108
+ // overloads route to their distinct graph nodes. Constructors are
109
+ // included so a `this(int)`/`super(int)` chain or `new Foo(int)`
110
+ // resolves to the matching ctor overload instead of first-wins
111
+ // collapsing onto another `Foo` ctor (a self-loop) — #1928 F38.
112
+ if (isOverloadableCallable(def.type) &&
101
113
  def.parameterTypes !== undefined &&
102
114
  def.parameterTypes.length > 0) {
103
115
  const pKey = qualifiedKey(filePath, def.type, `${qn}~${def.parameterTypes.join(',')}`);
@@ -87,9 +87,7 @@ export function buildGraphNodeLookup(graph) {
87
87
  // a parameter-types-suffixed key so resolveDefGraphId can find
88
88
  // the right overload by matching its def's parameterTypes.
89
89
  const pTypes = props.parameterTypes;
90
- if (pTypes !== undefined &&
91
- pTypes.length > 0 &&
92
- (node.label === 'Function' || node.label === 'Method')) {
90
+ if (pTypes !== undefined && pTypes.length > 0 && isOverloadableCallable(node.label)) {
93
91
  const pKey = qualifiedKey(props.filePath, node.label, `${keyQualified}~${pTypes.join(',')}`);
94
92
  // Each overload is unique — set unconditionally.
95
93
  if (!lookup.has(pKey))
@@ -98,7 +96,7 @@ export function buildGraphNodeLookup(graph) {
98
96
  const pClasses = props
99
97
  .parameterTypeClasses;
100
98
  const shapeTag = parameterShapeIdTag(pTypes, pClasses);
101
- if (shapeTag !== '' && (node.label === 'Function' || node.label === 'Method')) {
99
+ if (shapeTag !== '' && isOverloadableCallable(node.label)) {
102
100
  const shapeKey = qualifiedKey(props.filePath, node.label, `${keyQualified}${shapeTag}`);
103
101
  if (!lookup.has(shapeKey))
104
102
  lookup.set(shapeKey, node.id);
@@ -136,6 +134,17 @@ export function buildGraphNodeLookup(graph) {
136
134
  }
137
135
  return lookup;
138
136
  }
137
+ /**
138
+ * Callables whose same-name overloads must route to distinct graph nodes via
139
+ * the parameter-types / shape key. Constructors belong here too: a class with
140
+ * `Foo()` and `Foo(int)` mints distinct `#0`/`#1` Constructor nodes, and a
141
+ * `this(...)`/`super(...)` edge (or any `new Foo(args)`) must reach the right
142
+ * one. Without the overload key both ctor nodes collapse onto the first-wins
143
+ * qualified/simple key, turning a `this()` chain into a self-loop (#1928 F38).
144
+ */
145
+ function isOverloadableCallable(label) {
146
+ return label === 'Function' || label === 'Method' || label === 'Constructor';
147
+ }
139
148
  export function isLinkableLabel(label) {
140
149
  return (label === 'Function' ||
141
150
  label === 'Method' ||
@@ -15,25 +15,6 @@
15
15
  * from the same Database is the officially supported concurrency pattern.
16
16
  */
17
17
  import lbug from '@ladybugdb/core';
18
- /**
19
- * Probe whether a Windows FTS extension binary is locally installed under
20
- * ~/.lbdb/extension/<any-version>/win_amd64/fts/. Returns true on the first
21
- * version dir whose libfts.lbug_extension exists on disk; false if the
22
- * extension root is missing or contains no FTS binary.
23
- *
24
- * Gates the Windows skip-FTS-load guard below so we only skip the load
25
- * when no extension binary is present. When at least one binary exists,
26
- * loadFTSExtension is called with policy: 'load-only' — LadybugDB resolves
27
- * LOAD EXTENSION fts to its version-specific path internally, and the
28
- * ExtensionManager's tryLoad try/catch handles version-mismatch errors
29
- * cleanly without ever attempting dlopen of a stale binary. The install
30
- * path that the #1199/#1217 SIGSEGV documented is never exercised at
31
- * query time.
32
- *
33
- * Exported so unit tests can exercise the probe directly against a
34
- * temp-dir plus spied `os.homedir()` — see lbug-pool-win-fts-probe.test.ts.
35
- */
36
- export declare function hasLocalWinFtsExtension(): Promise<boolean>;
37
18
  /**
38
19
  * Listeners notified when a pool entry is torn down (LRU eviction, idle
39
20
  * timeout, explicit close). Used by upper layers (e.g. the BM25 search
@@ -15,49 +15,10 @@
15
15
  * from the same Database is the officially supported concurrency pattern.
16
16
  */
17
17
  import fs from 'fs/promises';
18
- import os from 'os';
19
- import path from 'path';
20
18
  import lbug from '@ladybugdb/core';
21
19
  import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
22
20
  import { createLbugDatabase, isWalCorruptionError, toNativeSafePath, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
23
21
  import { isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
24
- /**
25
- * Probe whether a Windows FTS extension binary is locally installed under
26
- * ~/.lbdb/extension/<any-version>/win_amd64/fts/. Returns true on the first
27
- * version dir whose libfts.lbug_extension exists on disk; false if the
28
- * extension root is missing or contains no FTS binary.
29
- *
30
- * Gates the Windows skip-FTS-load guard below so we only skip the load
31
- * when no extension binary is present. When at least one binary exists,
32
- * loadFTSExtension is called with policy: 'load-only' — LadybugDB resolves
33
- * LOAD EXTENSION fts to its version-specific path internally, and the
34
- * ExtensionManager's tryLoad try/catch handles version-mismatch errors
35
- * cleanly without ever attempting dlopen of a stale binary. The install
36
- * path that the #1199/#1217 SIGSEGV documented is never exercised at
37
- * query time.
38
- *
39
- * Exported so unit tests can exercise the probe directly against a
40
- * temp-dir plus spied `os.homedir()` — see lbug-pool-win-fts-probe.test.ts.
41
- */
42
- export async function hasLocalWinFtsExtension() {
43
- try {
44
- const extRoot = path.join(os.homedir(), '.lbdb', 'extension');
45
- const versions = await fs.readdir(extRoot);
46
- for (const v of versions) {
47
- try {
48
- await fs.stat(path.join(extRoot, v, 'win_amd64', 'fts', 'libfts.lbug_extension'));
49
- return true;
50
- }
51
- catch {
52
- /* missing for this version, keep looking */
53
- }
54
- }
55
- }
56
- catch {
57
- /* no .lbdb/extension dir */
58
- }
59
- return false;
60
- }
61
22
  const pool = new Map();
62
23
  const poolCloseListeners = new Set();
63
24
  /**
@@ -522,28 +483,7 @@ async function doInitLbug(repoId, dbPath) {
522
483
  // install; analyze owns extension installation. If LOAD fails, search
523
484
  // features degrade gracefully and the user-facing query path proceeds.
524
485
  if (!shared.ftsLoaded) {
525
- // Windows guard: LOAD EXTENSION fts crashes with SIGSEGV on Windows during
526
- // *install* — the @ladybugdb/core out-of-process installer hits an unhandled
527
- // error path that signals SIGSEGV instead of throwing (see #1199, #1217).
528
- // The previous unconditional skip was over-broad: it also disabled FTS on
529
- // hosts where the binary was already on disk and only needed LOAD, leaving
530
- // BM25 silently degraded with no error path (see #1690).
531
- //
532
- // Probe ~/.lbdb/extension/*/win_amd64/fts/ first. If any binary is on disk
533
- // we run loadFTSExtension(..., 'load-only'); the install path is never
534
- // exercised, and LadybugDB's version-specific resolution + ExtensionManager
535
- // try/catch handle stale/zero-byte siblings cleanly (verified empirically
536
- // on Win10 + Node 22.19 + gitnexus 1.6.5 + @ladybugdb/core 0.16.1). With
537
- // no binary at all, we fall back to the upstream skip so install-time
538
- // SIGSEGV continues to be avoided.
539
- if (process.platform === 'win32') {
540
- shared.ftsLoaded = (await hasLocalWinFtsExtension())
541
- ? await loadFTSExtension(available[0], { policy: 'load-only' })
542
- : true;
543
- }
544
- else {
545
- shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
546
- }
486
+ shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
547
487
  }
548
488
  // Register pool entry only after all connections are pre-warmed and FTS is
549
489
  // loaded. Concurrent executeQuery calls see either "not initialized"
@@ -599,16 +539,8 @@ export async function initLbugWithDb(repoId, existingDb, dbPath) {
599
539
  // Load FTS extension if not already loaded on this Database.
600
540
  // policy: 'load-only' — same contract as initLbug above; the read pool
601
541
  // must not block on a network install during query execution.
602
- // Windows guard: same probe-then-load policy as doInitLbug above.
603
542
  if (!shared.ftsLoaded) {
604
- if (process.platform === 'win32') {
605
- shared.ftsLoaded = (await hasLocalWinFtsExtension())
606
- ? await loadFTSExtension(available[0], { policy: 'load-only' })
607
- : true;
608
- }
609
- else {
610
- shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
611
- }
543
+ shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
612
544
  }
613
545
  pool.set(repoId, {
614
546
  db: existingDb,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.143",
3
+ "version": "1.6.6-rc.145",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -33,7 +33,7 @@ const PLATFORM_LOGIC = [
33
33
  'test/unit/resolve-invocation.test.ts',
34
34
  'test/unit/platform-capabilities.test.ts',
35
35
  'test/unit/worker-pool-windows-quarantine.test.ts',
36
- 'test/unit/lbug-pool-win-fts-probe.test.ts',
36
+ 'test/unit/lbug-pool-fts-load.test.ts',
37
37
  'test/unit/repo-manager.test.ts',
38
38
  'test/unit/repo-manager-finalize-invariant.test.ts',
39
39
  'test/unit/hooks.test.ts',