gitnexus 1.4.6 → 1.4.8

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 (99) hide show
  1. package/README.md +22 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index.js +2 -1
  7. package/dist/cli/setup.js +78 -1
  8. package/dist/config/supported-languages.d.ts +30 -0
  9. package/dist/config/supported-languages.js +30 -0
  10. package/dist/core/embeddings/embedder.d.ts +6 -1
  11. package/dist/core/embeddings/embedder.js +65 -5
  12. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  13. package/dist/core/embeddings/http-client.d.ts +31 -0
  14. package/dist/core/embeddings/http-client.js +179 -0
  15. package/dist/core/embeddings/index.d.ts +1 -0
  16. package/dist/core/embeddings/index.js +1 -0
  17. package/dist/core/embeddings/types.d.ts +1 -1
  18. package/dist/core/graph/types.d.ts +4 -3
  19. package/dist/core/ingestion/ast-helpers.d.ts +80 -0
  20. package/dist/core/ingestion/ast-helpers.js +738 -0
  21. package/dist/core/ingestion/call-analysis.d.ts +73 -0
  22. package/dist/core/ingestion/call-analysis.js +490 -0
  23. package/dist/core/ingestion/call-processor.d.ts +55 -2
  24. package/dist/core/ingestion/call-processor.js +673 -108
  25. package/dist/core/ingestion/call-routing.d.ts +23 -2
  26. package/dist/core/ingestion/call-routing.js +21 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  28. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  29. package/dist/core/ingestion/framework-detection.js +49 -12
  30. package/dist/core/ingestion/heritage-processor.js +47 -49
  31. package/dist/core/ingestion/import-processor.d.ts +1 -1
  32. package/dist/core/ingestion/import-processor.js +103 -194
  33. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  34. package/dist/core/ingestion/import-resolution.js +251 -0
  35. package/dist/core/ingestion/language-config.d.ts +3 -0
  36. package/dist/core/ingestion/language-config.js +13 -0
  37. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  38. package/dist/core/ingestion/markdown-processor.js +124 -0
  39. package/dist/core/ingestion/mro-processor.js +8 -3
  40. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  41. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  42. package/dist/core/ingestion/parsing-processor.d.ts +3 -2
  43. package/dist/core/ingestion/parsing-processor.js +27 -60
  44. package/dist/core/ingestion/pipeline.d.ts +10 -0
  45. package/dist/core/ingestion/pipeline.js +425 -4
  46. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  47. package/dist/core/ingestion/resolution-context.js +7 -4
  48. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  49. package/dist/core/ingestion/resolvers/index.js +1 -1
  50. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  51. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  52. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  53. package/dist/core/ingestion/resolvers/php.js +43 -3
  54. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  55. package/dist/core/ingestion/resolvers/utils.js +16 -0
  56. package/dist/core/ingestion/symbol-table.d.ts +29 -3
  57. package/dist/core/ingestion/symbol-table.js +42 -9
  58. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
  59. package/dist/core/ingestion/tree-sitter-queries.js +243 -2
  60. package/dist/core/ingestion/type-env.d.ts +28 -1
  61. package/dist/core/ingestion/type-env.js +451 -72
  62. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  63. package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
  64. package/dist/core/ingestion/type-extractors/csharp.js +189 -16
  65. package/dist/core/ingestion/type-extractors/go.js +45 -0
  66. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  67. package/dist/core/ingestion/type-extractors/index.js +1 -1
  68. package/dist/core/ingestion/type-extractors/jvm.js +244 -69
  69. package/dist/core/ingestion/type-extractors/php.js +31 -4
  70. package/dist/core/ingestion/type-extractors/python.js +89 -17
  71. package/dist/core/ingestion/type-extractors/ruby.js +17 -2
  72. package/dist/core/ingestion/type-extractors/rust.js +72 -4
  73. package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
  74. package/dist/core/ingestion/type-extractors/shared.js +115 -13
  75. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  76. package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
  77. package/dist/core/ingestion/type-extractors/typescript.js +171 -9
  78. package/dist/core/ingestion/utils.d.ts +2 -95
  79. package/dist/core/ingestion/utils.js +3 -892
  80. package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
  81. package/dist/core/ingestion/workers/parse-worker.js +116 -95
  82. package/dist/core/lbug/csv-generator.js +18 -1
  83. package/dist/core/lbug/lbug-adapter.d.ts +12 -0
  84. package/dist/core/lbug/lbug-adapter.js +71 -4
  85. package/dist/core/lbug/schema.d.ts +6 -4
  86. package/dist/core/lbug/schema.js +27 -3
  87. package/dist/mcp/core/embedder.js +11 -3
  88. package/dist/mcp/core/lbug-adapter.d.ts +22 -0
  89. package/dist/mcp/core/lbug-adapter.js +178 -23
  90. package/dist/mcp/local/local-backend.d.ts +22 -0
  91. package/dist/mcp/local/local-backend.js +136 -32
  92. package/dist/mcp/resources.js +13 -0
  93. package/dist/mcp/server.js +26 -4
  94. package/dist/mcp/tools.js +17 -7
  95. package/dist/server/api.d.ts +19 -1
  96. package/dist/server/api.js +66 -6
  97. package/dist/storage/git.d.ts +12 -0
  98. package/dist/storage/git.js +21 -0
  99. package/package.json +12 -4
@@ -1,5 +1,8 @@
1
1
  import { SupportedLanguages } from '../../../config/supported-languages.js';
2
+ import { type MixedChainStep } from '../utils.js';
2
3
  import type { ConstructorBinding } from '../type-env.js';
4
+ import type { NamedBinding } from '../import-resolution.js';
5
+ import type { NodeLabel } from '../../graph/types.js';
3
6
  interface ParsedNode {
4
7
  id: string;
5
8
  label: string;
@@ -14,6 +17,7 @@ interface ParsedNode {
14
17
  astFrameworkReason?: string;
15
18
  description?: string;
16
19
  parameterCount?: number;
20
+ requiredParameterCount?: number;
17
21
  returnType?: string;
18
22
  };
19
23
  }
@@ -21,7 +25,7 @@ interface ParsedRelationship {
21
25
  id: string;
22
26
  sourceId: string;
23
27
  targetId: string;
24
- type: 'DEFINES' | 'HAS_METHOD';
28
+ type: 'DEFINES' | 'HAS_METHOD' | 'HAS_PROPERTY';
25
29
  confidence: number;
26
30
  reason: string;
27
31
  }
@@ -29,9 +33,12 @@ interface ParsedSymbol {
29
33
  filePath: string;
30
34
  name: string;
31
35
  nodeId: string;
32
- type: string;
36
+ type: NodeLabel;
33
37
  parameterCount?: number;
38
+ requiredParameterCount?: number;
39
+ parameterTypes?: string[];
34
40
  returnType?: string;
41
+ declaredType?: string;
35
42
  ownerId?: string;
36
43
  }
37
44
  export interface ExtractedImport {
@@ -39,10 +46,7 @@ export interface ExtractedImport {
39
46
  rawImportPath: string;
40
47
  language: SupportedLanguages;
41
48
  /** Named bindings from the import (e.g., import {User as U} → [{local:'U', exported:'User'}]) */
42
- namedBindings?: {
43
- local: string;
44
- exported: string;
45
- }[];
49
+ namedBindings?: NamedBinding[];
46
50
  }
47
51
  export interface ExtractedCall {
48
52
  filePath: string;
@@ -57,13 +61,25 @@ export interface ExtractedCall {
57
61
  /** Resolved type name of the receiver (e.g., 'User' for user.save() when user: User) */
58
62
  receiverTypeName?: string;
59
63
  /**
60
- * Chained call names when the receiver is itself a call expression.
61
- * For `svc.getUser().save()`, the `save` ExtractedCall gets receiverCallChain = ['getUser']
62
- * with receiverName = 'svc'. The chain is ordered outermost-last, e.g.:
63
- * `a.b().c().d()` calledName='d', receiverCallChain=['b','c'], receiverName='a'
64
+ * Unified mixed chain when the receiver is a chain of field accesses and/or method calls.
65
+ * Steps are ordered base-first (innermost to outermost). Examples:
66
+ * `svc.getUser().save()` → chain=[{kind:'call',name:'getUser'}], receiverName='svc'
67
+ * `user.address.save()` chain=[{kind:'field',name:'address'}], receiverName='user'
68
+ * `svc.getUser().address.save()` → chain=[{kind:'call',name:'getUser'},{kind:'field',name:'address'}]
64
69
  * Length is capped at MAX_CHAIN_DEPTH (3).
65
70
  */
66
- receiverCallChain?: string[];
71
+ receiverMixedChain?: MixedChainStep[];
72
+ }
73
+ export interface ExtractedAssignment {
74
+ filePath: string;
75
+ /** generateId of enclosing function, or generateId('File', filePath) for top-level */
76
+ sourceId: string;
77
+ /** Receiver text (e.g., 'user' from user.address = value) */
78
+ receiverText: string;
79
+ /** Property name being written (e.g., 'address') */
80
+ propertyName: string;
81
+ /** Resolved type name of the receiver if available from TypeEnv */
82
+ receiverTypeName?: string;
67
83
  }
68
84
  export interface ExtractedHeritage {
69
85
  filePath: string;
@@ -87,15 +103,24 @@ export interface FileConstructorBindings {
87
103
  filePath: string;
88
104
  bindings: ConstructorBinding[];
89
105
  }
106
+ /** File-scope type bindings from TypeEnv fixpoint — used for cross-file ExportedTypeMap. */
107
+ export interface FileTypeEnvBindings {
108
+ filePath: string;
109
+ /** [varName, typeName] pairs from file scope (scope = '') */
110
+ bindings: [string, string][];
111
+ }
90
112
  export interface ParseWorkerResult {
91
113
  nodes: ParsedNode[];
92
114
  relationships: ParsedRelationship[];
93
115
  symbols: ParsedSymbol[];
94
116
  imports: ExtractedImport[];
95
117
  calls: ExtractedCall[];
118
+ assignments: ExtractedAssignment[];
96
119
  heritage: ExtractedHeritage[];
97
120
  routes: ExtractedRoute[];
98
121
  constructorBindings: FileConstructorBindings[];
122
+ /** File-scope type bindings from TypeEnv fixpoint for exported symbol collection. */
123
+ typeEnvBindings: FileTypeEnvBindings[];
99
124
  skippedLanguages: Record<string, number>;
100
125
  fileCount: number;
101
126
  }
@@ -28,15 +28,15 @@ try {
28
28
  Kotlin = _require('tree-sitter-kotlin');
29
29
  }
30
30
  catch { }
31
- import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, CALL_EXPRESSION_TYPES, extractCallChain, } from '../utils.js';
31
+ import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, getLabelFromCaptures, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from '../utils.js';
32
32
  import { buildTypeEnv } from '../type-env.js';
33
33
  import { isNodeExported } from '../export-detection.js';
34
34
  import { detectFrameworkFromAST } from '../framework-detection.js';
35
35
  import { typeConfigs } from '../type-extractors/index.js';
36
36
  import { generateId } from '../../../lib/utils.js';
37
- import { extractNamedBindings } from '../named-binding-extraction.js';
38
- import { appendKotlinWildcard } from '../resolvers/index.js';
37
+ import { namedBindingExtractors, preprocessImportPath } from '../import-resolution.js';
39
38
  import { callRouters } from '../call-routing.js';
39
+ import { extractPropertyDeclaredType } from '../type-extractors/shared.js';
40
40
  // ============================================================================
41
41
  // Worker-local parser + language map
42
42
  // ============================================================================
@@ -96,61 +96,7 @@ const findEnclosingFunctionId = (node, filePath) => {
96
96
  }
97
97
  return null;
98
98
  };
99
- // ============================================================================
100
- // Label detection from capture map
101
- // ============================================================================
102
- const getLabelFromCaptures = (captureMap) => {
103
- // Skip imports (handled separately) and calls
104
- if (captureMap['import'] || captureMap['call'])
105
- return null;
106
- if (!captureMap['name'])
107
- return null;
108
- if (captureMap['definition.function'])
109
- return 'Function';
110
- if (captureMap['definition.class'])
111
- return 'Class';
112
- if (captureMap['definition.interface'])
113
- return 'Interface';
114
- if (captureMap['definition.method'])
115
- return 'Method';
116
- if (captureMap['definition.struct'])
117
- return 'Struct';
118
- if (captureMap['definition.enum'])
119
- return 'Enum';
120
- if (captureMap['definition.namespace'])
121
- return 'Namespace';
122
- if (captureMap['definition.module'])
123
- return 'Module';
124
- if (captureMap['definition.trait'])
125
- return 'Trait';
126
- if (captureMap['definition.impl'])
127
- return 'Impl';
128
- if (captureMap['definition.type'])
129
- return 'TypeAlias';
130
- if (captureMap['definition.const'])
131
- return 'Const';
132
- if (captureMap['definition.static'])
133
- return 'Static';
134
- if (captureMap['definition.typedef'])
135
- return 'Typedef';
136
- if (captureMap['definition.macro'])
137
- return 'Macro';
138
- if (captureMap['definition.union'])
139
- return 'Union';
140
- if (captureMap['definition.property'])
141
- return 'Property';
142
- if (captureMap['definition.record'])
143
- return 'Record';
144
- if (captureMap['definition.delegate'])
145
- return 'Delegate';
146
- if (captureMap['definition.annotation'])
147
- return 'Annotation';
148
- if (captureMap['definition.constructor'])
149
- return 'Constructor';
150
- if (captureMap['definition.template'])
151
- return 'Template';
152
- return 'CodeElement';
153
- };
99
+ // Label detection moved to shared getLabelFromCaptures in utils.ts
154
100
  // DEFINITION_CAPTURE_KEYS and getDefinitionNodeFromCaptures imported from ../utils.js
155
101
  // ============================================================================
156
102
  // Process a batch of files
@@ -162,9 +108,11 @@ const processBatch = (files, onProgress) => {
162
108
  symbols: [],
163
109
  imports: [],
164
110
  calls: [],
111
+ assignments: [],
165
112
  heritage: [],
166
113
  routes: [],
167
114
  constructorBindings: [],
115
+ typeEnvBindings: [],
168
116
  skippedLanguages: {},
169
117
  fileCount: 0,
170
118
  };
@@ -725,13 +673,6 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
725
673
  }
726
674
  result.fileCount++;
727
675
  onFileProcessed?.();
728
- // Build per-file type environment + constructor bindings in a single AST walk.
729
- // Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
730
- const typeEnv = buildTypeEnv(tree, language);
731
- const callRouter = callRouters[language];
732
- if (typeEnv.constructorBindings.length > 0) {
733
- result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
734
- }
735
676
  let matches;
736
677
  try {
737
678
  matches = query.matches(tree.rootNode);
@@ -740,6 +681,51 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
740
681
  console.warn(`Query execution failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
741
682
  continue;
742
683
  }
684
+ // Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
685
+ // Heritage edges (EXTENDS/IMPLEMENTS) are created by heritage-processor which runs
686
+ // in PARALLEL with call-processor, so the graph edges don't exist when buildTypeEnv
687
+ // runs. This pre-pass makes parent class information available for type resolution.
688
+ const fileParentMap = new Map();
689
+ for (const match of matches) {
690
+ const captureMap = {};
691
+ for (const c of match.captures) {
692
+ captureMap[c.name] = c.node;
693
+ }
694
+ if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
695
+ const className = captureMap['heritage.class'].text;
696
+ const parentName = captureMap['heritage.extends'].text;
697
+ // Skip Go named fields (only anonymous fields are struct embedding)
698
+ const extendsNode = captureMap['heritage.extends'];
699
+ const fieldDecl = extendsNode.parent;
700
+ if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
701
+ continue;
702
+ let parents = fileParentMap.get(className);
703
+ if (!parents) {
704
+ parents = [];
705
+ fileParentMap.set(className, parents);
706
+ }
707
+ if (!parents.includes(parentName))
708
+ parents.push(parentName);
709
+ }
710
+ }
711
+ // Build per-file type environment + constructor bindings in a single AST walk.
712
+ // Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
713
+ const parentMap = fileParentMap;
714
+ const typeEnv = buildTypeEnv(tree, language, { parentMap });
715
+ const callRouter = callRouters[language];
716
+ if (typeEnv.constructorBindings.length > 0) {
717
+ result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
718
+ }
719
+ // Extract file-scope bindings for ExportedTypeMap (closes worker/sequential quality gap).
720
+ // Sequential path uses collectExportedBindings(typeEnv) directly; worker path serializes
721
+ // these bindings so the main thread can merge them into ExportedTypeMap.
722
+ const fileScope = typeEnv.env.get('');
723
+ if (fileScope && fileScope.size > 0) {
724
+ const bindings = [];
725
+ for (const [name, type] of fileScope)
726
+ bindings.push([name, type]);
727
+ result.typeEnvBindings.push({ filePath: file.path, bindings });
728
+ }
743
729
  for (const match of matches) {
744
730
  const captureMap = {};
745
731
  for (const c of match.captures) {
@@ -747,10 +733,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
747
733
  }
748
734
  // Extract import paths before skipping
749
735
  if (captureMap['import'] && captureMap['import.source']) {
750
- const rawImportPath = language === SupportedLanguages.Kotlin
751
- ? appendKotlinWildcard(captureMap['import.source'].text.replace(/['"<>]/g, ''), captureMap['import'])
752
- : captureMap['import.source'].text.replace(/['"<>]/g, '');
753
- const namedBindings = extractNamedBindings(captureMap['import'], language);
736
+ const rawImportPath = preprocessImportPath(captureMap['import.source'].text, captureMap['import'], language);
737
+ if (!rawImportPath)
738
+ continue;
739
+ const extractor = namedBindingExtractors[language];
740
+ const namedBindings = extractor ? extractor(captureMap['import']) : undefined;
754
741
  result.imports.push({
755
742
  filePath: file.path,
756
743
  rawImportPath,
@@ -759,6 +746,28 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
759
746
  });
760
747
  continue;
761
748
  }
749
+ // Extract assignment sites (field write access)
750
+ if (captureMap['assignment'] && captureMap['assignment.receiver'] && captureMap['assignment.property']) {
751
+ const receiverText = captureMap['assignment.receiver'].text;
752
+ const propertyName = captureMap['assignment.property'].text;
753
+ if (receiverText && propertyName) {
754
+ const srcId = findEnclosingFunctionId(captureMap['assignment'], file.path)
755
+ || generateId('File', file.path);
756
+ let receiverTypeName;
757
+ if (typeEnv) {
758
+ receiverTypeName = typeEnv.lookup(receiverText, captureMap['assignment']) ?? undefined;
759
+ }
760
+ result.assignments.push({
761
+ filePath: file.path,
762
+ sourceId: srcId,
763
+ receiverText,
764
+ propertyName,
765
+ ...(receiverTypeName ? { receiverTypeName } : {}),
766
+ });
767
+ }
768
+ if (!captureMap['call'])
769
+ continue;
770
+ }
762
771
  // Extract call sites
763
772
  if (captureMap['call']) {
764
773
  const callNameNode = captureMap['call.name'];
@@ -811,6 +820,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
811
820
  nodeId,
812
821
  type: 'Property',
813
822
  ...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
823
+ ...(item.declaredType ? { declaredType: item.declaredType } : {}),
814
824
  });
815
825
  const fileId = generateId('File', file.path);
816
826
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
@@ -824,10 +834,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
824
834
  });
825
835
  if (propEnclosingClassId) {
826
836
  result.relationships.push({
827
- id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
837
+ id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
828
838
  sourceId: propEnclosingClassId,
829
839
  targetId: nodeId,
830
- type: 'HAS_METHOD',
840
+ type: 'HAS_PROPERTY',
831
841
  confidence: 1.0,
832
842
  reason: '',
833
843
  });
@@ -844,26 +854,19 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
844
854
  const callForm = inferCallForm(callNode, callNameNode);
845
855
  let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
846
856
  let receiverTypeName = receiverName ? typeEnv.lookup(receiverName, callNode) : undefined;
847
- let receiverCallChain;
848
- // When the receiver is a call_expression (e.g. svc.getUser().save()),
849
- // extractReceiverName returns undefined because it refuses complex expressions.
850
- // Instead, walk the receiver node to build a call chain for deferred resolution.
851
- // We capture the base receiver name so processCallsFromExtracted can look it up
852
- // from constructor bindings. receiverTypeName is intentionally left unset here —
853
- // the chain resolver in processCallsFromExtracted needs the base type as input and
854
- // produces the final receiver type as output.
857
+ let receiverMixedChain;
858
+ // When the receiver is a complex expression (call chain, field chain, or mixed),
859
+ // extractReceiverName returns undefined. Walk the receiver node to build a unified
860
+ // mixed chain for deferred resolution in processCallsFromExtracted.
855
861
  if (callForm === 'member' && receiverName === undefined && !receiverTypeName) {
856
862
  const receiverNode = extractReceiverNode(callNameNode);
857
- if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) {
858
- const extracted = extractCallChain(receiverNode);
859
- if (extracted) {
860
- receiverCallChain = extracted.chain;
861
- // Set receiverName to the base object so Step 1 in processCallsFromExtracted
862
- // can resolve it via constructor bindings to a base type for the chain.
863
+ if (receiverNode) {
864
+ const extracted = extractMixedChain(receiverNode);
865
+ if (extracted && extracted.chain.length > 0) {
866
+ receiverMixedChain = extracted.chain;
863
867
  receiverName = extracted.baseReceiverName;
864
- // Also try the type environment immediately (covers explicitly-typed locals
865
- // and annotated parameters like `fn process(svc: &UserService)`).
866
- // This sets a base type that chain resolution (Step 2) will use as input.
868
+ // Try the type environment immediately for the base receiver
869
+ // (covers explicitly-typed locals and annotated parameters).
867
870
  if (receiverName) {
868
871
  receiverTypeName = typeEnv.lookup(receiverName, callNode);
869
872
  }
@@ -878,7 +881,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
878
881
  ...(callForm !== undefined ? { callForm } : {}),
879
882
  ...(receiverName !== undefined ? { receiverName } : {}),
880
883
  ...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
881
- ...(receiverCallChain !== undefined ? { receiverCallChain } : {}),
884
+ ...(receiverMixedChain !== undefined ? { receiverMixedChain } : {}),
882
885
  });
883
886
  }
884
887
  }
@@ -923,7 +926,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
923
926
  continue;
924
927
  }
925
928
  }
926
- const nodeLabel = getLabelFromCaptures(captureMap);
929
+ const nodeLabel = getLabelFromCaptures(captureMap, language);
927
930
  if (!nodeLabel)
928
931
  continue;
929
932
  const nameNode = captureMap['name'];
@@ -947,10 +950,15 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
947
950
  ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
948
951
  : null;
949
952
  let parameterCount;
953
+ let requiredParameterCount;
954
+ let parameterTypes;
950
955
  let returnType;
956
+ let declaredType;
951
957
  if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
952
958
  const sig = extractMethodSignature(definitionNode);
953
959
  parameterCount = sig.parameterCount;
960
+ requiredParameterCount = sig.requiredParameterCount;
961
+ parameterTypes = sig.parameterTypes;
954
962
  returnType = sig.returnType;
955
963
  // Language-specific return type fallback (e.g. Ruby YARD @return [Type])
956
964
  // Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
@@ -963,6 +971,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
963
971
  }
964
972
  }
965
973
  }
974
+ else if (nodeLabel === 'Property' && definitionNode) {
975
+ // Extract the declared type for property/field nodes.
976
+ // Walk the definition node for type annotation children.
977
+ declaredType = extractPropertyDeclaredType(definitionNode);
978
+ }
966
979
  result.nodes.push({
967
980
  id: nodeId,
968
981
  label: nodeLabel,
@@ -979,6 +992,8 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
979
992
  } : {}),
980
993
  ...(description !== undefined ? { description } : {}),
981
994
  ...(parameterCount !== undefined ? { parameterCount } : {}),
995
+ ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
996
+ ...(parameterTypes !== undefined ? { parameterTypes } : {}),
982
997
  ...(returnType !== undefined ? { returnType } : {}),
983
998
  },
984
999
  });
@@ -992,7 +1007,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
992
1007
  nodeId,
993
1008
  type: nodeLabel,
994
1009
  ...(parameterCount !== undefined ? { parameterCount } : {}),
1010
+ ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
1011
+ ...(parameterTypes !== undefined ? { parameterTypes } : {}),
995
1012
  ...(returnType !== undefined ? { returnType } : {}),
1013
+ ...(declaredType !== undefined ? { declaredType } : {}),
996
1014
  ...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
997
1015
  });
998
1016
  const fileId = generateId('File', file.path);
@@ -1005,13 +1023,14 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1005
1023
  confidence: 1.0,
1006
1024
  reason: '',
1007
1025
  });
1008
- // ── HAS_METHOD: link method/constructor/property to enclosing class ──
1026
+ // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
1009
1027
  if (enclosingClassId) {
1028
+ const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
1010
1029
  result.relationships.push({
1011
- id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
1030
+ id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
1012
1031
  sourceId: enclosingClassId,
1013
1032
  targetId: nodeId,
1014
- type: 'HAS_METHOD',
1033
+ type: memberEdgeType,
1015
1034
  confidence: 1.0,
1016
1035
  reason: '',
1017
1036
  });
@@ -1030,7 +1049,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1030
1049
  /** Accumulated result across sub-batches */
1031
1050
  let accumulated = {
1032
1051
  nodes: [], relationships: [], symbols: [],
1033
- imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
1052
+ imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0,
1034
1053
  };
1035
1054
  let cumulativeProcessed = 0;
1036
1055
  const mergeResult = (target, src) => {
@@ -1039,9 +1058,11 @@ const mergeResult = (target, src) => {
1039
1058
  target.symbols.push(...src.symbols);
1040
1059
  target.imports.push(...src.imports);
1041
1060
  target.calls.push(...src.calls);
1061
+ target.assignments.push(...src.assignments);
1042
1062
  target.heritage.push(...src.heritage);
1043
1063
  target.routes.push(...src.routes);
1044
1064
  target.constructorBindings.push(...src.constructorBindings);
1065
+ target.typeEnvBindings.push(...src.typeEnvBindings);
1045
1066
  for (const [lang, count] of Object.entries(src.skippedLanguages)) {
1046
1067
  target.skippedLanguages[lang] = (target.skippedLanguages[lang] || 0) + count;
1047
1068
  }
@@ -1064,7 +1085,7 @@ parentPort.on('message', (msg) => {
1064
1085
  if (msg && msg.type === 'flush') {
1065
1086
  parentPort.postMessage({ type: 'result', data: accumulated });
1066
1087
  // Reset for potential reuse
1067
- accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
1088
+ accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 };
1068
1089
  cumulativeProcessed = 0;
1069
1090
  return;
1070
1091
  }
@@ -208,6 +208,8 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
208
208
  const codeElemWriter = new BufferedCSVWriter(path.join(csvDir, 'codeelement.csv'), codeElementHeader);
209
209
  const communityWriter = new BufferedCSVWriter(path.join(csvDir, 'community.csv'), 'id,label,heuristicLabel,keywords,description,enrichedBy,cohesion,symbolCount');
210
210
  const processWriter = new BufferedCSVWriter(path.join(csvDir, 'process.csv'), 'id,label,heuristicLabel,processType,stepCount,communities,entryPointId,terminalId');
211
+ // Section nodes have an extra 'level' column
212
+ const sectionWriter = new BufferedCSVWriter(path.join(csvDir, 'section.csv'), 'id,name,filePath,startLine,endLine,level,content,description');
211
213
  // Multi-language node types share the same CSV shape (no isExported column)
212
214
  const multiLangHeader = 'id,name,filePath,startLine,endLine,content,description';
213
215
  const MULTI_LANG_TYPES = ['Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
@@ -292,6 +294,20 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
292
294
  ].join(','));
293
295
  break;
294
296
  }
297
+ case 'Section': {
298
+ const content = await extractContent(node, contentCache);
299
+ await sectionWriter.addRow([
300
+ escapeCSVField(node.id),
301
+ escapeCSVField(node.properties.name || ''),
302
+ escapeCSVField(node.properties.filePath || ''),
303
+ escapeCSVNumber(node.properties.startLine, -1),
304
+ escapeCSVNumber(node.properties.endLine, -1),
305
+ escapeCSVNumber(node.properties.level, 1),
306
+ escapeCSVField(content),
307
+ escapeCSVField(node.properties.description || ''),
308
+ ].join(','));
309
+ break;
310
+ }
295
311
  default: {
296
312
  // Code element nodes (Function, Class, Interface, CodeElement)
297
313
  const writer = codeWriterMap[node.label];
@@ -329,7 +345,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
329
345
  }
330
346
  }
331
347
  // Finish all node writers
332
- const allWriters = [fileWriter, folderWriter, functionWriter, classWriter, interfaceWriter, methodWriter, codeElemWriter, communityWriter, processWriter, ...multiLangWriters.values()];
348
+ const allWriters = [fileWriter, folderWriter, functionWriter, classWriter, interfaceWriter, methodWriter, codeElemWriter, communityWriter, processWriter, sectionWriter, ...multiLangWriters.values()];
333
349
  await Promise.all(allWriters.map(w => w.finish()));
334
350
  // --- Stream relationship CSV ---
335
351
  const relCsvPath = path.join(csvDir, 'relations.csv');
@@ -353,6 +369,7 @@ export const streamAllCSVsToDisk = async (graph, repoPath, csvDir) => {
353
369
  ['Interface', interfaceWriter], ['Method', methodWriter],
354
370
  ['CodeElement', codeElemWriter],
355
371
  ['Community', communityWriter], ['Process', processWriter],
372
+ ['Section', sectionWriter],
356
373
  ...Array.from(multiLangWriters.entries()).map(([name, w]) => [name, w]),
357
374
  ];
358
375
  for (const [name, writer] of tableMap) {
@@ -1,5 +1,13 @@
1
1
  import lbug from '@ladybugdb/core';
2
2
  import { KnowledgeGraph } from '../graph/types.js';
3
+ /** Expose the current Database for pool adapter reuse in tests. */
4
+ export declare const getDatabase: () => lbug.Database | null;
5
+ /**
6
+ * Return true when the error message indicates that another process holds
7
+ * an exclusive lock on the LadybugDB file (e.g. `gitnexus analyze` or
8
+ * `gitnexus serve` running at the same time).
9
+ */
10
+ export declare const isDbBusyError: (err: unknown) => boolean;
3
11
  export declare const initLbug: (dbPath: string) => Promise<{
4
12
  db: lbug.Database;
5
13
  conn: lbug.Connection;
@@ -7,6 +15,10 @@ export declare const initLbug: (dbPath: string) => Promise<{
7
15
  /**
8
16
  * Execute multiple queries against one repo DB atomically.
9
17
  * While the callback runs, no other request can switch the active DB.
18
+ *
19
+ * Automatically retries up to DB_LOCK_RETRY_ATTEMPTS times when the
20
+ * database is busy (e.g. `gitnexus analyze` holds the write lock).
21
+ * Each retry waits DB_LOCK_RETRY_DELAY_MS * attempt milliseconds.
10
22
  */
11
23
  export declare const withLbugDb: <T>(dbPath: string, operation: () => Promise<T>) => Promise<T>;
12
24
  export type LbugProgressCallback = (message: string) => void;