gitnexus 1.6.13-rc.77 → 1.6.13-rc.79

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.
@@ -108,7 +108,7 @@ export async function augment(pattern, cwd) {
108
108
  await initLbug(repoId, repo.lbugPath);
109
109
  }
110
110
  // Step 1: BM25 search (fast, no embeddings)
111
- const { results: bm25Results, ftsAvailable } = await searchFTSFromLbug(pattern, 10, repoId);
111
+ const { results: bm25Results } = await searchFTSFromLbug(pattern, 10, repoId);
112
112
  // Step 2: Map BM25 file results to symbols
113
113
  const symbolMatches = [];
114
114
  for (const result of bm25Results.slice(0, 5)) {
@@ -135,14 +135,16 @@ export async function augment(pattern, cwd) {
135
135
  /* skip */
136
136
  }
137
137
  }
138
- // When FTS indexes are unavailable (read-only DB, first run before indexes are built),
139
- // fall back to a direct name CONTAINS query so enrichment still works.
140
- if (symbolMatches.length === 0 && !ftsAvailable) {
138
+ // FTS ranks files by mentions, so a widely referenced symbol's definition
139
+ // may not be present in the top file results. Fall back to graph names
140
+ // whenever those files produce no symbol match, regardless of FTS health.
141
+ if (symbolMatches.length === 0) {
141
142
  const fallbackRows = await executeQuery(repoId, `
142
143
  MATCH (n)
143
144
  WHERE n.name CONTAINS '${patternFirstWord}'
144
- RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
145
- ORDER BY id
145
+ RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath,
146
+ CASE WHEN n.name = '${patternFirstWord}' THEN 0 ELSE 1 END AS exactRank
147
+ ORDER BY exactRank, id
146
148
  LIMIT 5
147
149
  `).catch(() => []);
148
150
  for (const sym of fallbackRows) {
@@ -1,6 +1,6 @@
1
1
  import Go from 'tree-sitter-go';
2
2
  import { goImportPackageName } from '../../../ingestion/languages/go/import-package-name.js';
3
- import { stringLiteral } from '../../../ingestion/route-extractors/go-shared.js';
3
+ import { isEchoImportPath, isGinImportPath, stringLiteral, } from '../../../ingestion/route-extractors/go-shared.js';
4
4
  import { compilePatterns, runCompiledPatterns, } from '../tree-sitter-scanner.js';
5
5
  /**
6
6
  * Go HTTP plugin. Handles:
@@ -54,7 +54,8 @@ const HANDLER_ARG_TYPES = new Set([
54
54
  ]);
55
55
  /**
56
56
  * The file's framework import aliases: which local qualifiers resolve to
57
- * echo and to gin. Matched on the import path rather than the local name, so
57
+ * echo and to gin. Matched on the EXACT import path (shared with ingestion via
58
+ * go-shared.ts) rather than the local name, so
58
59
  * an aliased import still counts; an unaliased import is keyed by its
59
60
  * conventional package name (`goImportPackageName`). `_` and `.` imports bind
60
61
  * no qualifier this file can route through. An empty set means the file
@@ -77,9 +78,9 @@ function readFrameworkImports(root) {
77
78
  const local = spec.childForFieldName('name')?.text ?? goImportPackageName(importPath);
78
79
  if (local === '_' || local === '.')
79
80
  continue;
80
- if (importPath.includes('labstack/echo'))
81
+ if (isEchoImportPath(importPath))
81
82
  echo.add(local);
82
- else if (importPath.includes('gin-gonic/gin'))
83
+ else if (isGinImportPath(importPath))
83
84
  gin.add(local);
84
85
  }
85
86
  return { echo, gin };
@@ -139,10 +140,22 @@ function asGroupCall(node) {
139
140
  return null;
140
141
  return { parent, prefix };
141
142
  }
143
+ /** The `var_spec`s of a `var_declaration`, flattened out of a grouped `var (…)`. */
144
+ function varSpecs(decl) {
145
+ return decl.namedChildren
146
+ .flatMap((c) => (c.type === 'var_spec_list' ? c.namedChildren : [c]))
147
+ .filter((spec) => spec.type === 'var_spec');
148
+ }
142
149
  /** The expression `name` is assigned by `stmt` (`:=`, `=`, or `var`), if any. */
143
150
  function boundValue(stmt, name) {
144
151
  const pick = (names, values) => {
145
- const i = names.findIndex((n) => n.type === 'identifier' && n.text === name);
152
+ // Go carries an assignment out left to right, so a repeated target leaves
153
+ // the LAST one in place (`g, g = a, b` ends with g == b).
154
+ let i = -1;
155
+ for (let k = 0; k < names.length; k++) {
156
+ if (names[k].type === 'identifier' && names[k].text === name)
157
+ i = k;
158
+ }
146
159
  if (i < 0)
147
160
  return undefined;
148
161
  return codeChildren(values)[i] ?? null;
@@ -154,10 +167,7 @@ function boundValue(stmt, name) {
154
167
  case 'var_spec':
155
168
  return pick(stmt.childrenForFieldName('name'), stmt.childForFieldName('value'));
156
169
  case 'var_declaration': {
157
- const specs = stmt.namedChildren.flatMap((c) => c.type === 'var_spec_list' ? c.namedChildren : [c]);
158
- for (const spec of specs) {
159
- if (spec.type !== 'var_spec')
160
- continue;
170
+ for (const spec of varSpecs(stmt)) {
161
171
  const value = pick(spec.childrenForFieldName('name'), spec.childForFieldName('value'));
162
172
  if (value !== undefined)
163
173
  return value;
@@ -196,20 +206,200 @@ function within(outer, inner) {
196
206
  return !!outer && outer.startIndex <= inner.startIndex && inner.endIndex <= outer.endIndex;
197
207
  }
198
208
  /**
199
- * Whether `stmt` writes `name` with a plain assignment somewhere inside it
200
- * (`g = …` in a nested block, branch, loop, or closure body). A nested `:=`
201
- * declares a new variable and is not a write to the outer one.
209
+ * Whether `node` is itself a write to `name`: a plain `assignment_statement`
210
+ * (`g = …`) or the `=` form of a range clause (`for _, g = range xs`) or a
211
+ * select receive (`case g = <-ch`), which tree-sitter parses as their own node
212
+ * types. A `:=` declares a new variable — loop- or case-local — and is not a
213
+ * write to the outer one.
214
+ */
215
+ function isWriteTo(node, name) {
216
+ if (node.type === 'assignment_statement') {
217
+ return declaresName(node.childForFieldName('left'), name);
218
+ }
219
+ return ((node.type === 'range_clause' || node.type === 'receive_statement') &&
220
+ assignsExistingName(node) &&
221
+ declaresName(node.childForFieldName('left'), name));
222
+ }
223
+ /**
224
+ * Every write to `name` inside `scope` (a statement or a function literal).
225
+ * With `executedOnly`, a nested function literal is entered only through a
226
+ * call: one merely stored in a value does not run when its enclosing body
227
+ * runs.
228
+ */
229
+ function writesInScope(scope, name, executedOnly) {
230
+ const out = [];
231
+ const walk = (node) => {
232
+ if (node.type === 'func_literal') {
233
+ if (executedOnly && !isInvokedLiteral(node))
234
+ return;
235
+ const body = node.childForFieldName('body');
236
+ if (body)
237
+ walk(body);
238
+ return;
239
+ }
240
+ if (isWriteTo(node, name))
241
+ out.push(node);
242
+ for (const child of node.namedChildren)
243
+ walk(child);
244
+ };
245
+ walk(scope);
246
+ return out;
247
+ }
248
+ /**
249
+ * Whether `scope` writes `name` through a binding declared OUTSIDE it: an
250
+ * assignment (or `=` range/receive) whose target resolves to a name the scope
251
+ * itself does not declare — a captured outer group rather than a local `:=`,
252
+ * `var`, or parameter. An unresolvable or conflicting write reaches out
253
+ * conservatively. With `executedOnly`, writes that only run if a nested
254
+ * function literal is later called are ignored.
255
+ */
256
+ function writesOuterName(scope, name, executedOnly = false) {
257
+ for (const write of writesInScope(scope, name, executedOnly)) {
258
+ const ident = identifierNamed(write.childForFieldName('left'), name);
259
+ if (!ident)
260
+ continue;
261
+ // A range/receive `=` target resolves from OUTSIDE its clause: the
262
+ // clause's own binding logic describes the loop/case variable, not the
263
+ // existing name the `=` writes.
264
+ const from = write.type === 'range_clause' || write.type === 'receive_statement'
265
+ ? write.parent
266
+ : undefined;
267
+ const binding = lookupBinding(ident, from);
268
+ // null is a declaration without a traceable value; it carries no position
269
+ // to place, so a write through it reaches out conservatively.
270
+ if (binding === undefined || binding === null || binding === CONFLICT)
271
+ return true;
272
+ const declared = isParamBinding(binding) ? binding.param : binding;
273
+ if (!within(scope, declared))
274
+ return true;
275
+ }
276
+ return false;
277
+ }
278
+ /**
279
+ * Whether `stmt` writes `name` through a binding declared outside it — a
280
+ * nested assignment, branch, loop, or closure body. A declaration that
281
+ * shadows `name` inside the statement (a nested `:=`, a parameter) writes the
282
+ * local, not the target the use would see, so it does not count.
202
283
  */
203
284
  function writesNameInside(stmt, name) {
204
- return [stmt, ...stmt.descendantsOfType('assignment_statement')].some((a) => a.type === 'assignment_statement' && declaresName(a.childForFieldName('left'), name));
285
+ return writesOuterName(stmt, name);
205
286
  }
206
- /** Whether an identifier or expression_list (e.g. a range left side) declares `name`. */
207
- function declaresName(node, name) {
287
+ /** A range clause or receive that assigns (`=`) rather than declares (`:=`). */
288
+ function assignsExistingName(clause) {
289
+ return !clause.children.some((c) => c.type === ':=');
290
+ }
291
+ /**
292
+ * Whether `fn` is the callee of a call (`func(){ … }()`), ignoring
293
+ * parentheses, so its body is known to run.
294
+ */
295
+ function isInvokedLiteral(fn) {
296
+ let callee = fn;
297
+ while (callee.parent?.type === 'parenthesized_expression')
298
+ callee = callee.parent;
299
+ const parent = callee.parent;
300
+ return (!!parent &&
301
+ parent.type === 'call_expression' &&
302
+ parent.childForFieldName('function')?.id === callee.id);
303
+ }
304
+ /**
305
+ * Whether evaluating `node` executes a write to `name`. A function literal is
306
+ * only entered when it is the callee of a call: `g := func(){ g = … }` merely
307
+ * stores the literal, so the outer `g` is untouched, while `func(){ … }()`
308
+ * runs its body (writes that resolve to the literal's own locals excepted).
309
+ */
310
+ function executedWritesName(node, name) {
311
+ if (node.type === 'func_literal') {
312
+ return isInvokedLiteral(node) && writesOuterName(node, name, true);
313
+ }
314
+ if (isWriteTo(node, name))
315
+ return true;
316
+ return node.namedChildren.some((child) => executedWritesName(child, name));
317
+ }
318
+ /**
319
+ * Whether the initializer of the declaration that brings `name` into scope in
320
+ * a block writes the OUTER `name`. The right-hand side runs before the new
321
+ * variable exists, so `g := func(){ g = … }()` still writes the binding the
322
+ * use outside the block would inherit — but a literal only stored in a value
323
+ * never runs its body. Grouped `var` specs up to and including the declaring
324
+ * one are scanned; later specs read the new local.
325
+ */
326
+ function initializerWritesName(decl, name) {
327
+ if (decl.type === 'short_var_declaration') {
328
+ const values = decl.childForFieldName('right');
329
+ return !!values && executedWritesName(values, name);
330
+ }
331
+ if (decl.type === 'var_declaration') {
332
+ for (const spec of varSpecs(decl)) {
333
+ const value = spec.childForFieldName('value');
334
+ if (value && executedWritesName(value, name))
335
+ return true;
336
+ if (boundValue(spec, name) !== undefined)
337
+ return false;
338
+ }
339
+ }
340
+ return false;
341
+ }
342
+ /**
343
+ * Whether `stmt` holds a function literal that writes the outer `name`. Such
344
+ * a literal can run after any later direct assignment (`reset := func(){ g = … };
345
+ * g = …; reset()`), so captured writes are found independently of statement
346
+ * order instead of being lost behind the last-assignment return. Writes that
347
+ * resolve to a declaration inside the literal (a parameter, a nested `:=`) are
348
+ * local and do not count.
349
+ */
350
+ function closureWritesName(stmt, name) {
351
+ const literals = stmt.type === 'func_literal'
352
+ ? [stmt, ...stmt.descendantsOfType('func_literal')]
353
+ : stmt.descendantsOfType('func_literal');
354
+ return literals.some((fn) => writesOuterName(fn, name));
355
+ }
356
+ /**
357
+ * The effect a bare `{ … }` block placed before the use has on the outer
358
+ * `name`. A bare block always runs, so its last top-level `name = v` is the
359
+ * value that reaches the use (`{ g = r.Group("/new") }` → `/new`); a write
360
+ * nested in a branch, loop, or closure inside it is CONFLICT. A `:=` or `var`
361
+ * of `name` in the block starts a new variable, so only the statements before
362
+ * it touch the outer one — but the declaring statement's initializer still
363
+ * does, because it evaluates before the new variable is in scope. undefined
364
+ * when the block leaves `name` alone.
365
+ */
366
+ function bareBlockWrite(block, name) {
367
+ const stmts = codeChildren(block);
368
+ const declIndex = stmts.findIndex((s) => (s.type === 'short_var_declaration' || s.type === 'var_declaration') &&
369
+ boundValue(s, name) !== undefined);
370
+ const outerRegion = declIndex < 0 ? stmts : stmts.slice(0, declIndex);
371
+ if (declIndex >= 0 && initializerWritesName(stmts[declIndex], name))
372
+ return CONFLICT;
373
+ if (outerRegion.some((stmt) => closureWritesName(stmt, name)))
374
+ return CONFLICT;
375
+ for (const stmt of outerRegion.reverse()) {
376
+ if (stmt.type === 'assignment_statement') {
377
+ const value = boundValue(stmt, name);
378
+ if (value !== undefined)
379
+ return value;
380
+ }
381
+ if (stmt.type === 'block') {
382
+ const inner = bareBlockWrite(stmt, name);
383
+ if (inner !== undefined)
384
+ return inner;
385
+ continue;
386
+ }
387
+ if (writesNameInside(stmt, name))
388
+ return CONFLICT;
389
+ }
390
+ return undefined;
391
+ }
392
+ /** The identifier named `name` on an assignment side (or null). */
393
+ function identifierNamed(node, name) {
208
394
  if (!node)
209
- return false;
395
+ return null;
210
396
  if (node.type === 'identifier')
211
- return node.text === name;
212
- return node.namedChildren.some((n) => n.type === 'identifier' && n.text === name);
397
+ return node.text === name ? node : null;
398
+ return node.namedChildren.find((n) => n.type === 'identifier' && n.text === name) ?? null;
399
+ }
400
+ /** Whether an identifier or expression_list (e.g. a range left side) declares `name`. */
401
+ function declaresName(node, name) {
402
+ return identifierNamed(node, name) !== null;
213
403
  }
214
404
  /**
215
405
  * The value last bound to identifier `ident` before its use: the nearest
@@ -234,12 +424,14 @@ function findBinding(ident) {
234
424
  * CONFLICT means a preceding statement writes the name in a nested scope, so
235
425
  * the value at the use is control-flow dependent. A parameter or method
236
426
  * receiver of the enclosing function returns its declaration (ParamBinding):
237
- * no value, but a static type.
427
+ * no value, but a static type. `from` starts the walk at that node's parent
428
+ * instead of the identifier's, so an `=` range/receive target can be resolved
429
+ * outside its own clause.
238
430
  */
239
- function lookupBinding(ident) {
431
+ function lookupBinding(ident, from) {
240
432
  const name = ident.text;
241
- let child = ident;
242
- for (let node = ident.parent; node; child = node, node = node.parent) {
433
+ let child = from ?? ident;
434
+ for (let node = child.parent; node; child = node, node = node.parent) {
243
435
  if (node.type === 'function_declaration' ||
244
436
  node.type === 'method_declaration' ||
245
437
  node.type === 'func_literal') {
@@ -287,8 +479,15 @@ function lookupBinding(ident) {
287
479
  const value = boundValue(stmt, name);
288
480
  if (value !== undefined)
289
481
  return value;
290
- // A write nested in an earlier statement (block, branch, loop) may or
291
- // may not run before the use: the reaching value is unprovable.
482
+ // A bare block always runs: its unconditional writes are bindings.
483
+ if (stmt.type === 'block') {
484
+ const written = bareBlockWrite(stmt, name);
485
+ if (written !== undefined)
486
+ return written;
487
+ continue;
488
+ }
489
+ // A write nested in an earlier branch, loop, or closure may or may
490
+ // not run before the use: the reaching value is unprovable.
292
491
  if (writesNameInside(stmt, name))
293
492
  return CONFLICT;
294
493
  }
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { goImportPackageName } from '../languages/go/import-package-name.js';
26
26
  import { GoRouteBindings } from '../languages/go/route-bindings.js';
27
- import { stringLiteral } from './go-shared.js';
27
+ import { isEchoImportPath, isGinImportPath, stringLiteral } from './go-shared.js';
28
28
  import { normalizeExtractedRoutePath } from './route-path.js';
29
29
  export const GIN_ROUTE_SOURCE = 'gin-route';
30
30
  export const ECHO_ROUTE_SOURCE = 'echo-route';
@@ -58,9 +58,9 @@ function readImports(root) {
58
58
  const local = explicit ?? goImportPackageName(importPath);
59
59
  if (!local)
60
60
  continue;
61
- if (importPath === 'github.com/gin-gonic/gin')
61
+ if (isGinImportPath(importPath))
62
62
  gin = local;
63
- if (/^github\.com\/labstack\/echo(\/v\d+)?$/.test(importPath))
63
+ if (isEchoImportPath(importPath))
64
64
  echo = local;
65
65
  }
66
66
  // Both, or neither: no way to tell which argument is the handler.
@@ -1,4 +1,12 @@
1
1
  import type { SyntaxNode } from 'tree-sitter';
2
+ /**
3
+ * Framework import paths, matched EXACTLY on both layers (ingestion's
4
+ * `go-gin-echo.ts` and the group-mode `http-patterns/go.ts`): a substring
5
+ * test would classify `example.com/labstack/echo-wrapper` as echo on one
6
+ * side only, flipping the handler-argument order and splitting contracts.
7
+ */
8
+ export declare function isGinImportPath(importPath: string): boolean;
9
+ export declare function isEchoImportPath(importPath: string): boolean;
2
10
  /**
3
11
  * Go string-literal decoding shared by the Go route extractors on both
4
12
  * layers: the ingestion extractor (`go-gin-echo.ts`, Strategy A) and the
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Framework import paths, matched EXACTLY on both layers (ingestion's
3
+ * `go-gin-echo.ts` and the group-mode `http-patterns/go.ts`): a substring
4
+ * test would classify `example.com/labstack/echo-wrapper` as echo on one
5
+ * side only, flipping the handler-argument order and splitting contracts.
6
+ */
7
+ export function isGinImportPath(importPath) {
8
+ return importPath === 'github.com/gin-gonic/gin';
9
+ }
10
+ export function isEchoImportPath(importPath) {
11
+ return /^github\.com\/labstack\/echo(\/v\d+)?$/.test(importPath);
12
+ }
1
13
  /**
2
14
  * Go string-literal decoding shared by the Go route extractors on both
3
15
  * layers: the ingestion extractor (`go-gin-echo.ts`, Strategy A) and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.13-rc.77",
3
+ "version": "1.6.13-rc.79",
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",