gitnexus 1.4.5 → 1.4.6

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 (30) hide show
  1. package/dist/cli/eval-server.js +13 -5
  2. package/dist/cli/index.js +0 -0
  3. package/dist/cli/tool.d.ts +3 -2
  4. package/dist/cli/tool.js +48 -13
  5. package/dist/core/ingestion/call-processor.d.ts +0 -1
  6. package/dist/core/ingestion/call-processor.js +1 -132
  7. package/dist/core/ingestion/parsing-processor.js +5 -2
  8. package/dist/core/ingestion/symbol-table.d.ts +6 -0
  9. package/dist/core/ingestion/symbol-table.js +21 -1
  10. package/dist/core/ingestion/type-env.js +62 -10
  11. package/dist/core/ingestion/type-extractors/c-cpp.js +2 -2
  12. package/dist/core/ingestion/type-extractors/csharp.js +21 -7
  13. package/dist/core/ingestion/type-extractors/go.js +41 -10
  14. package/dist/core/ingestion/type-extractors/jvm.js +47 -20
  15. package/dist/core/ingestion/type-extractors/php.js +142 -4
  16. package/dist/core/ingestion/type-extractors/python.js +21 -7
  17. package/dist/core/ingestion/type-extractors/ruby.js +2 -2
  18. package/dist/core/ingestion/type-extractors/rust.js +25 -12
  19. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -0
  20. package/dist/core/ingestion/type-extractors/shared.js +133 -1
  21. package/dist/core/ingestion/type-extractors/types.d.ts +44 -12
  22. package/dist/core/ingestion/type-extractors/typescript.js +22 -8
  23. package/dist/core/ingestion/workers/parse-worker.js +5 -2
  24. package/dist/mcp/local/local-backend.d.ts +1 -0
  25. package/dist/mcp/local/local-backend.js +23 -1
  26. package/hooks/claude/gitnexus-hook.cjs +0 -0
  27. package/hooks/claude/pre-tool-use.sh +0 -0
  28. package/hooks/claude/session-start.sh +0 -0
  29. package/package.json +3 -2
  30. package/scripts/patch-tree-sitter-swift.cjs +0 -0
@@ -132,7 +132,7 @@ export function resolveIterableElementType(iterableName, node, scopeEnv, declara
132
132
  /** Known single-arg nullable wrapper types that unwrap to their inner type
133
133
  * for receiver resolution. Optional<User> → "User", Option<User> → "User".
134
134
  * Only nullable wrappers — NOT containers (List, Vec) or async wrappers (Promise, Future).
135
- * See call-processor.ts WRAPPER_GENERICS for the full set used in return-type inference. */
135
+ * See WRAPPER_GENERICS below for the full set used in return-type inference. */
136
136
  const NULLABLE_WRAPPER_TYPES = new Set([
137
137
  'Optional', // Java
138
138
  'Option', // Rust, Scala
@@ -569,3 +569,135 @@ export function extractElementTypeFromString(typeStr, pos = 'last') {
569
569
  }
570
570
  return undefined;
571
571
  }
572
+ // ── Return type text helpers ─────────────────────────────────────────────
573
+ // extractReturnTypeName works on raw return-type text already stored in
574
+ // SymbolDefinition (e.g. "User", "Promise<User>", "User | null", "*User").
575
+ // Extracts the base user-defined type name.
576
+ /** Primitive / built-in types that should NOT produce a receiver binding. */
577
+ const PRIMITIVE_TYPES = new Set([
578
+ 'string', 'number', 'boolean', 'void', 'int', 'float', 'double', 'long',
579
+ 'short', 'byte', 'char', 'bool', 'str', 'i8', 'i16', 'i32', 'i64',
580
+ 'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'usize', 'isize',
581
+ 'undefined', 'null', 'None', 'nil',
582
+ ]);
583
+ /**
584
+ * Extract a simple type name from raw return-type text.
585
+ * Handles common patterns:
586
+ * "User" → "User"
587
+ * "Promise<User>" → "User" (unwrap wrapper generics)
588
+ * "Option<User>" → "User"
589
+ * "Result<User, Error>" → "User" (first type arg)
590
+ * "User | null" → "User" (strip nullable union)
591
+ * "User?" → "User" (strip nullable suffix)
592
+ * "*User" → "User" (Go pointer)
593
+ * "&User" → "User" (Rust reference)
594
+ * Returns undefined for complex types or primitives.
595
+ */
596
+ const WRAPPER_GENERICS = new Set([
597
+ 'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask', // async wrappers
598
+ 'Option', 'Some', 'Optional', 'Maybe', // nullable wrappers
599
+ 'Result', 'Either', // result wrappers
600
+ // Rust smart pointers (Deref to inner type)
601
+ 'Rc', 'Arc', 'Weak', // pointer types
602
+ 'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', // guard types
603
+ 'Ref', 'RefMut', // RefCell guards
604
+ 'Cow', // copy-on-write
605
+ // Containers (List, Array, Vec, Set, etc.) are intentionally excluded —
606
+ // methods are called on the container, not the element type.
607
+ // Non-wrapper generics return the base type (e.g., List) via the else branch.
608
+ ]);
609
+ /**
610
+ * Extracts the first type argument from a comma-separated generic argument string,
611
+ * respecting nested angle brackets. For example:
612
+ * "Result<User, Error>" → "Result<User, Error>" (no top-level comma)
613
+ * "User, Error" → "User"
614
+ * "Map<K, V>, string" → "Map<K, V>"
615
+ */
616
+ function extractFirstGenericArg(args) {
617
+ let depth = 0;
618
+ for (let i = 0; i < args.length; i++) {
619
+ if (args[i] === '<')
620
+ depth++;
621
+ else if (args[i] === '>')
622
+ depth--;
623
+ else if (args[i] === ',' && depth === 0)
624
+ return args.slice(0, i).trim();
625
+ }
626
+ return args.trim();
627
+ }
628
+ /**
629
+ * Extract the first non-lifetime type argument from a generic argument string.
630
+ * Skips Rust lifetime parameters (e.g., `'a`, `'_`) to find the actual type.
631
+ * "'_, User" → "User"
632
+ * "'a, User" → "User"
633
+ * "User, Error" → "User" (no lifetime — delegates to extractFirstGenericArg)
634
+ */
635
+ function extractFirstTypeArg(args) {
636
+ let remaining = args;
637
+ while (remaining) {
638
+ const first = extractFirstGenericArg(remaining);
639
+ if (!first.startsWith("'"))
640
+ return first;
641
+ // Skip past this lifetime arg + the comma separator
642
+ const commaIdx = remaining.indexOf(',', first.length);
643
+ if (commaIdx < 0)
644
+ return first; // only lifetimes — fall through
645
+ remaining = remaining.slice(commaIdx + 1).trim();
646
+ }
647
+ return args.trim();
648
+ }
649
+ const MAX_RETURN_TYPE_INPUT_LENGTH = 2048;
650
+ const MAX_RETURN_TYPE_LENGTH = 512;
651
+ export const extractReturnTypeName = (raw, depth = 0) => {
652
+ if (depth > 10)
653
+ return undefined;
654
+ if (raw.length > MAX_RETURN_TYPE_INPUT_LENGTH)
655
+ return undefined;
656
+ let text = raw.trim();
657
+ if (!text)
658
+ return undefined;
659
+ // Strip pointer/reference prefixes: *User, &User, &mut User
660
+ text = text.replace(/^[&*]+\s*(mut\s+)?/, '');
661
+ // Strip nullable suffix: User?
662
+ text = text.replace(/\?$/, '');
663
+ // Handle union types: "User | null" → "User"
664
+ if (text.includes('|')) {
665
+ const parts = text.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil');
666
+ if (parts.length === 1)
667
+ text = parts[0];
668
+ else
669
+ return undefined; // genuine union — too complex
670
+ }
671
+ // Handle generics: Promise<User> → unwrap if wrapper, else take base
672
+ const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
673
+ if (genericMatch) {
674
+ const [, base, args] = genericMatch;
675
+ if (WRAPPER_GENERICS.has(base)) {
676
+ // Take the first non-lifetime type argument, using bracket-balanced splitting
677
+ // so that nested generics like Result<User, Error> are not split at the inner
678
+ // comma. Lifetime parameters (Rust 'a, '_) are skipped.
679
+ const firstArg = extractFirstTypeArg(args);
680
+ return extractReturnTypeName(firstArg, depth + 1);
681
+ }
682
+ // Non-wrapper generic: return the base type (e.g., Map<K,V> → Map)
683
+ return PRIMITIVE_TYPES.has(base.toLowerCase()) ? undefined : base;
684
+ }
685
+ // Bare wrapper type without generic argument (e.g. Task, Promise, Option)
686
+ // should not produce a binding — these are meaningless without a type parameter
687
+ if (WRAPPER_GENERICS.has(text))
688
+ return undefined;
689
+ // Handle qualified names: models.User → User, Models::User → User, \App\Models\User → User
690
+ if (text.includes('::') || text.includes('.') || text.includes('\\')) {
691
+ text = text.split(/::|[.\\]/).pop();
692
+ }
693
+ // Final check: skip primitives
694
+ if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase()))
695
+ return undefined;
696
+ // Must start with uppercase (class/type convention) or be a valid identifier
697
+ if (!/^[A-Z_]\w*$/.test(text))
698
+ return undefined;
699
+ // If the final extracted type name is too long, reject it
700
+ if (text.length > MAX_RETURN_TYPE_LENGTH)
701
+ return undefined;
702
+ return text;
703
+ };
@@ -23,17 +23,48 @@ export type ConstructorBindingScanner = (node: SyntaxNode) => {
23
23
  * Used for languages where return types are expressed in comments (e.g. YARD @return [Type])
24
24
  * rather than in AST fields. Returns undefined if no return type can be determined. */
25
25
  export type ReturnTypeExtractor = (node: SyntaxNode) => string | undefined;
26
- /** Extracts loop variable type binding from a for-each statement.
27
- * All parameters are required (aligned with PatternBindingExtractor convention)
28
- * to prevent new extractors from silently ignoring declarationTypeNodes/scope. */
29
- export type ForLoopExtractor = (node: SyntaxNode, scopeEnv: Map<string, string>, declarationTypeNodes: ReadonlyMap<string, SyntaxNode>, scope: string) => void;
30
- /** Extracts a plain-identifier assignment for Tier 2 propagation.
31
- * For `const b = a`, returns { lhs: 'b', rhs: 'a' } when the LHS has no resolved type.
32
- * Returns undefined if the node is not a plain identifier assignment. */
33
- export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => {
26
+ /** Narrow lookup interface for resolving a callee name → return type name.
27
+ * Backed by SymbolTable.lookupFuzzyCallable; passed via ForLoopExtractorContext.
28
+ * Conservative: returns undefined when the callee is ambiguous (0 or 2+ matches). */
29
+ export interface ReturnTypeLookup {
30
+ /** Processed type name after stripping wrappers (e.g., 'User' from 'Promise<User>').
31
+ * Use for call-result variable bindings (`const b = foo()`). */
32
+ lookupReturnType(callee: string): string | undefined;
33
+ /** Raw return type as declared in the symbol (e.g., '[]User', 'List<User>').
34
+ * Use for iterable-element extraction (`for v := range foo()`). */
35
+ lookupRawReturnType(callee: string): string | undefined;
36
+ }
37
+ /** Context object passed to ForLoopExtractor.
38
+ * Groups the four parameters that were previously positional. */
39
+ export interface ForLoopExtractorContext {
40
+ /** Mutable type-env for the current scope — extractor writes bindings here */
41
+ scopeEnv: Map<string, string>;
42
+ /** Maps `scope\0varName` to the declaration's type annotation AST node */
43
+ declarationTypeNodes: ReadonlyMap<string, SyntaxNode>;
44
+ /** Current scope key, e.g. `"process@42"` */
45
+ scope: string;
46
+ /** Resolves a callee name to its declared return type (undefined = unknown/ambiguous) */
47
+ returnTypeLookup: ReturnTypeLookup;
48
+ }
49
+ /** Extracts loop variable type binding from a for-each statement. */
50
+ export type ForLoopExtractor = (node: SyntaxNode, ctx: ForLoopExtractorContext) => void;
51
+ /** Discriminated union for pending Tier-2 propagation items.
52
+ * - `copy` — `const b = a` (identifier alias, propagate a's type to b)
53
+ * - `callResult` — `const b = foo()` (bind b to foo's declared return type) */
54
+ export type PendingAssignment = {
55
+ kind: 'copy';
34
56
  lhs: string;
35
57
  rhs: string;
36
- } | undefined;
58
+ } | {
59
+ kind: 'callResult';
60
+ lhs: string;
61
+ callee: string;
62
+ };
63
+ /** Extracts a pending assignment for Tier 2 propagation.
64
+ * Returns a PendingAssignment when the RHS is a bare identifier (`copy`) or a
65
+ * call expression (`callResult`) and the LHS has no resolved type yet.
66
+ * Returns undefined if the node is not a matching assignment. */
67
+ export type PendingAssignmentExtractor = (node: SyntaxNode, scopeEnv: ReadonlyMap<string, string>) => PendingAssignment | undefined;
37
68
  /** Extracts a typed variable binding from a pattern-matching construct.
38
69
  * Returns { varName, typeName } for patterns that introduce NEW variables.
39
70
  * Examples: `if let Some(user) = opt` (Rust), `x instanceof User user` (Java).
@@ -82,9 +113,10 @@ export interface LanguageTypeConfig {
82
113
  extractReturnType?: ReturnTypeExtractor;
83
114
  /** Extract loop variable → type binding from a for-each AST node. */
84
115
  extractForLoopBinding?: ForLoopExtractor;
85
- /** Extract plain-identifier assignment (e.g. `const b = a`) for Tier 2 chain propagation.
86
- * Called on declaration/assignment nodes; returns {lhs, rhs} when the RHS is a bare identifier
87
- * and the LHS has no resolved type yet. Language-specific because AST shapes differ widely. */
116
+ /** Extract pending assignment for Tier 2 propagation.
117
+ * Called on declaration/assignment nodes; returns a PendingAssignment when the RHS
118
+ * is a bare identifier (copy) or call expression (callResult) and the LHS has no
119
+ * resolved type yet. Language-specific because AST shapes differ widely. */
88
120
  extractPendingAssignment?: PendingAssignmentExtractor;
89
121
  /** Extract a typed variable binding from a pattern-matching construct.
90
122
  * Called on every AST node; returns { varName, typeName } when the node introduces a new
@@ -345,7 +345,7 @@ const findTsIterableElementType = (iterableName, startNode, pos = 'last') => {
345
345
  * 3. AST walk — walks up to the enclosing function's parameters to read User[] annotations directly
346
346
  * Only handles `for...of`; `for...in` produces string keys, not element types.
347
347
  */
348
- const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
348
+ const extractForLoopBinding = (node, { scopeEnv, declarationTypeNodes, scope, returnTypeLookup }) => {
349
349
  if (node.type !== 'for_in_statement')
350
350
  return;
351
351
  // Confirm this is `for...of`, not `for...in`, by scanning unnamed children for the keyword text.
@@ -359,10 +359,11 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
359
359
  }
360
360
  if (!isForOf)
361
361
  return;
362
- // The iterable is the `right` field — may be identifier or call_expression.
362
+ // The iterable is the `right` field — may be identifier, member_expression, or call_expression.
363
363
  const rightNode = node.childForFieldName('right');
364
364
  let iterableName;
365
365
  let methodName;
366
+ let callExprElementType;
366
367
  if (rightNode?.type === 'identifier') {
367
368
  iterableName = rightNode.text;
368
369
  }
@@ -374,6 +375,7 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
374
375
  else if (rightNode?.type === 'call_expression') {
375
376
  // entries.values() → call_expression > function: member_expression > object + property
376
377
  // this.repos.values() → nested member_expression: extract property from inner member
378
+ // getUsers() → call_expression > function: identifier (Phase 7.3 — return-type path)
377
379
  const fn = rightNode.childForFieldName('function');
378
380
  if (fn?.type === 'member_expression') {
379
381
  const obj = fn.childForFieldName('object');
@@ -390,13 +392,25 @@ const extractForLoopBinding = (node, scopeEnv, declarationTypeNodes, scope) => {
390
392
  if (prop?.type === 'property_identifier')
391
393
  methodName = prop.text;
392
394
  }
395
+ else if (fn?.type === 'identifier') {
396
+ // Direct function call: for (const user of getUsers())
397
+ const rawReturn = returnTypeLookup.lookupRawReturnType(fn.text);
398
+ if (rawReturn)
399
+ callExprElementType = extractElementTypeFromString(rawReturn);
400
+ }
393
401
  }
394
- if (!iterableName)
402
+ if (!iterableName && !callExprElementType)
395
403
  return;
396
- // Look up the container's base type name for descriptor-aware resolution
397
- const containerTypeName = scopeEnv.get(iterableName);
398
- const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
399
- const elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractTsElementTypeFromAnnotation, findTsIterableElementType, typeArgPos);
404
+ let elementType;
405
+ if (callExprElementType) {
406
+ elementType = callExprElementType;
407
+ }
408
+ else {
409
+ // Look up the container's base type name for descriptor-aware resolution
410
+ const containerTypeName = scopeEnv.get(iterableName);
411
+ const typeArgPos = methodToTypeArgPosition(methodName, containerTypeName);
412
+ elementType = resolveIterableElementType(iterableName, node, scopeEnv, declarationTypeNodes, scope, extractTsElementTypeFromAnnotation, findTsIterableElementType, typeArgPos);
413
+ }
400
414
  if (!elementType)
401
415
  return;
402
416
  // The loop variable is the `left` field.
@@ -444,7 +458,7 @@ const extractPendingAssignment = (node, scopeEnv) => {
444
458
  if (scopeEnv.has(lhs))
445
459
  continue;
446
460
  if (valueNode.type === 'identifier')
447
- return { lhs, rhs: valueNode.text };
461
+ return { kind: 'copy', lhs, rhs: valueNode.text };
448
462
  }
449
463
  return undefined;
450
464
  };
@@ -953,10 +953,13 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
953
953
  parameterCount = sig.parameterCount;
954
954
  returnType = sig.returnType;
955
955
  // Language-specific return type fallback (e.g. Ruby YARD @return [Type])
956
- if (!returnType && definitionNode) {
956
+ // Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
957
+ if ((!returnType || returnType === 'array' || returnType === 'iterable') && definitionNode) {
957
958
  const tc = typeConfigs[language];
958
959
  if (tc?.extractReturnType) {
959
- returnType = tc.extractReturnType(definitionNode);
960
+ const docReturn = tc.extractReturnType(definitionNode);
961
+ if (docReturn)
962
+ returnType = docReturn;
960
963
  }
961
964
  }
962
965
  }
@@ -144,6 +144,7 @@ export declare class LocalBackend {
144
144
  */
145
145
  private rename;
146
146
  private impact;
147
+ private _impactImpl;
147
148
  /**
148
149
  * Query clusters (communities) directly from graph.
149
150
  * Used by getClustersResource — avoids legacy overview() dispatch.
@@ -37,7 +37,7 @@ export const VALID_NODE_LABELS = new Set([
37
37
  'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module',
38
38
  ]);
39
39
  /** Valid relation types for impact analysis filtering */
40
- export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']);
40
+ export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES']);
41
41
  /** Regex to detect write operations in user-supplied Cypher queries */
42
42
  export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
43
43
  /** Check if a Cypher query contains write operations */
@@ -1189,6 +1189,22 @@ export class LocalBackend {
1189
1189
  };
1190
1190
  }
1191
1191
  async impact(repo, params) {
1192
+ try {
1193
+ return await this._impactImpl(repo, params);
1194
+ }
1195
+ catch (err) {
1196
+ // Return structured error instead of crashing (#321)
1197
+ return {
1198
+ error: (err instanceof Error ? err.message : String(err)) || 'Impact analysis failed',
1199
+ target: { name: params.target },
1200
+ direction: params.direction,
1201
+ impactedCount: 0,
1202
+ risk: 'UNKNOWN',
1203
+ suggestion: 'The graph query failed — try gitnexus context <symbol> as a fallback',
1204
+ };
1205
+ }
1206
+ }
1207
+ async _impactImpl(repo, params) {
1192
1208
  await this.ensureInitialized(repo.id);
1193
1209
  const { target, direction } = params;
1194
1210
  const maxDepth = params.maxDepth || 3;
@@ -1213,6 +1229,7 @@ export class LocalBackend {
1213
1229
  const impacted = [];
1214
1230
  const visited = new Set([symId]);
1215
1231
  let frontier = [symId];
1232
+ let traversalComplete = true;
1216
1233
  for (let depth = 1; depth <= maxDepth && frontier.length > 0; depth++) {
1217
1234
  const nextFrontier = [];
1218
1235
  // Batch frontier nodes into a single Cypher query per depth level
@@ -1244,6 +1261,10 @@ export class LocalBackend {
1244
1261
  }
1245
1262
  catch (e) {
1246
1263
  logQueryError('impact:depth-traversal', e);
1264
+ // Break out of depth loop on query failure but return partial results
1265
+ // collected so far, rather than silently swallowing the error (#321)
1266
+ traversalComplete = false;
1267
+ break;
1247
1268
  }
1248
1269
  frontier = nextFrontier;
1249
1270
  }
@@ -1321,6 +1342,7 @@ export class LocalBackend {
1321
1342
  direction,
1322
1343
  impactedCount: impacted.length,
1323
1344
  risk,
1345
+ ...(!traversalComplete && { partial: true }),
1324
1346
  summary: {
1325
1347
  direct: directCount,
1326
1348
  processes_affected: processCount,
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
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",
@@ -45,7 +45,8 @@
45
45
  "test:watch": "vitest",
46
46
  "test:coverage": "vitest run --coverage",
47
47
  "prepare": "npm run build",
48
- "postinstall": "node scripts/patch-tree-sitter-swift.cjs"
48
+ "postinstall": "node scripts/patch-tree-sitter-swift.cjs",
49
+ "prepack": "npm run build && chmod +x dist/cli/index.js"
49
50
  },
50
51
  "dependencies": {
51
52
  "@huggingface/transformers": "^3.0.0",
File without changes