gitnexus 1.4.6 → 1.4.7

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 (40) hide show
  1. package/dist/core/graph/types.d.ts +2 -2
  2. package/dist/core/ingestion/call-processor.d.ts +7 -1
  3. package/dist/core/ingestion/call-processor.js +308 -104
  4. package/dist/core/ingestion/call-routing.d.ts +17 -2
  5. package/dist/core/ingestion/call-routing.js +21 -0
  6. package/dist/core/ingestion/parsing-processor.d.ts +2 -1
  7. package/dist/core/ingestion/parsing-processor.js +32 -6
  8. package/dist/core/ingestion/pipeline.js +5 -1
  9. package/dist/core/ingestion/symbol-table.d.ts +13 -3
  10. package/dist/core/ingestion/symbol-table.js +23 -4
  11. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
  12. package/dist/core/ingestion/tree-sitter-queries.js +200 -0
  13. package/dist/core/ingestion/type-env.js +94 -38
  14. package/dist/core/ingestion/type-extractors/c-cpp.js +27 -2
  15. package/dist/core/ingestion/type-extractors/csharp.js +40 -0
  16. package/dist/core/ingestion/type-extractors/go.js +45 -0
  17. package/dist/core/ingestion/type-extractors/jvm.js +75 -3
  18. package/dist/core/ingestion/type-extractors/php.js +31 -4
  19. package/dist/core/ingestion/type-extractors/python.js +89 -17
  20. package/dist/core/ingestion/type-extractors/ruby.js +17 -2
  21. package/dist/core/ingestion/type-extractors/rust.js +37 -3
  22. package/dist/core/ingestion/type-extractors/shared.d.ts +12 -0
  23. package/dist/core/ingestion/type-extractors/shared.js +110 -3
  24. package/dist/core/ingestion/type-extractors/types.d.ts +17 -4
  25. package/dist/core/ingestion/type-extractors/typescript.js +30 -0
  26. package/dist/core/ingestion/utils.d.ts +25 -0
  27. package/dist/core/ingestion/utils.js +160 -1
  28. package/dist/core/ingestion/workers/parse-worker.d.ts +23 -7
  29. package/dist/core/ingestion/workers/parse-worker.js +68 -26
  30. package/dist/core/lbug/lbug-adapter.d.ts +2 -0
  31. package/dist/core/lbug/lbug-adapter.js +2 -0
  32. package/dist/core/lbug/schema.d.ts +1 -1
  33. package/dist/core/lbug/schema.js +1 -1
  34. package/dist/mcp/core/lbug-adapter.d.ts +22 -0
  35. package/dist/mcp/core/lbug-adapter.js +167 -23
  36. package/dist/mcp/local/local-backend.js +3 -3
  37. package/dist/mcp/resources.js +11 -0
  38. package/dist/mcp/server.js +26 -4
  39. package/dist/mcp/tools.js +15 -5
  40. package/package.json +4 -4
@@ -254,7 +254,7 @@ export const CLASS_CONTAINER_TYPES = new Set([
254
254
  'class_declaration', 'abstract_class_declaration',
255
255
  'interface_declaration', 'struct_declaration', 'record_declaration',
256
256
  'class_specifier', 'struct_specifier',
257
- 'impl_item', 'trait_item',
257
+ 'impl_item', 'trait_item', 'struct_item', 'enum_item',
258
258
  'class_definition',
259
259
  'trait_declaration',
260
260
  'protocol_declaration',
@@ -275,6 +275,8 @@ export const CONTAINER_TYPE_TO_LABEL = {
275
275
  class_definition: 'Class',
276
276
  impl_item: 'Impl',
277
277
  trait_item: 'Trait',
278
+ struct_item: 'Struct',
279
+ enum_item: 'Enum',
278
280
  trait_declaration: 'Trait',
279
281
  record_declaration: 'Record',
280
282
  protocol_declaration: 'Interface',
@@ -306,6 +308,21 @@ export const findEnclosingClassId = (node, filePath) => {
306
308
  }
307
309
  }
308
310
  }
311
+ // Go: type_declaration wrapping a struct_type (type User struct { ... })
312
+ // field_declaration → field_declaration_list → struct_type → type_spec → type_declaration
313
+ if (current.type === 'type_declaration') {
314
+ const typeSpec = current.children?.find((c) => c.type === 'type_spec');
315
+ if (typeSpec) {
316
+ const typeBody = typeSpec.childForFieldName?.('type');
317
+ if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
318
+ const nameNode = typeSpec.childForFieldName?.('name');
319
+ if (nameNode) {
320
+ const label = typeBody.type === 'struct_type' ? 'Struct' : 'Interface';
321
+ return generateId(label, `${filePath}:${nameNode.text}`);
322
+ }
323
+ }
324
+ }
325
+ }
309
326
  if (CLASS_CONTAINER_TYPES.has(current.type)) {
310
327
  // Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
311
328
  if (current.type === 'impl_item') {
@@ -1129,3 +1146,145 @@ export function extractCallChain(receiverCallNode) {
1129
1146
  }
1130
1147
  return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
1131
1148
  }
1149
+ /** Node types representing member/field access across languages. */
1150
+ const FIELD_ACCESS_NODE_TYPES = new Set([
1151
+ 'member_expression', // TS/JS
1152
+ 'member_access_expression', // C#
1153
+ 'selector_expression', // Go
1154
+ 'field_expression', // Rust/C++
1155
+ 'field_access', // Java
1156
+ 'attribute', // Python
1157
+ 'navigation_expression', // Kotlin/Swift
1158
+ 'member_binding_expression', // C# null-conditional (user?.Address)
1159
+ ]);
1160
+ /**
1161
+ * Walk a receiver AST node that may interleave field accesses and method calls,
1162
+ * building a unified chain of steps up to MAX_CHAIN_DEPTH.
1163
+ *
1164
+ * For `svc.getUser().address.save()`, called with the receiver of `save`
1165
+ * (`svc.getUser().address`, a field access node):
1166
+ * returns { chain: [{ kind:'call', name:'getUser' }, { kind:'field', name:'address' }],
1167
+ * baseReceiverName: 'svc' }
1168
+ *
1169
+ * For `user.getAddress().city.getName()`, called with receiver of `getName`
1170
+ * (`user.getAddress().city`):
1171
+ * returns { chain: [{ kind:'call', name:'getAddress' }, { kind:'field', name:'city' }],
1172
+ * baseReceiverName: 'user' }
1173
+ *
1174
+ * Pure field chains and pure call chains are special cases (all steps same kind).
1175
+ */
1176
+ export function extractMixedChain(receiverNode) {
1177
+ const chain = [];
1178
+ let current = receiverNode;
1179
+ while (chain.length < MAX_CHAIN_DEPTH) {
1180
+ if (CALL_EXPRESSION_TYPES.has(current.type)) {
1181
+ // ── Call expression: extract method name + inner receiver ────────────
1182
+ const funcNode = current.childForFieldName?.('function')
1183
+ ?? current.childForFieldName?.('name')
1184
+ ?? current.childForFieldName?.('method');
1185
+ let methodName;
1186
+ let innerReceiver = null;
1187
+ if (funcNode) {
1188
+ methodName = funcNode.lastNamedChild?.text ?? funcNode.text;
1189
+ }
1190
+ // Kotlin/Swift: call_expression → navigation_expression
1191
+ if (!funcNode && current.type === 'call_expression') {
1192
+ const callee = current.firstNamedChild;
1193
+ if (callee?.type === 'navigation_expression') {
1194
+ const suffix = callee.lastNamedChild;
1195
+ if (suffix?.type === 'navigation_suffix') {
1196
+ methodName = suffix.lastNamedChild?.text;
1197
+ for (let i = 0; i < callee.namedChildCount; i++) {
1198
+ const child = callee.namedChild(i);
1199
+ if (child && child.type !== 'navigation_suffix') {
1200
+ innerReceiver = child;
1201
+ break;
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ if (!methodName)
1208
+ break;
1209
+ chain.unshift({ kind: 'call', name: methodName });
1210
+ if (!innerReceiver && funcNode) {
1211
+ innerReceiver = funcNode.childForFieldName?.('object')
1212
+ ?? funcNode.childForFieldName?.('value')
1213
+ ?? funcNode.childForFieldName?.('operand')
1214
+ ?? funcNode.childForFieldName?.('argument') // C/C++ field_expression
1215
+ ?? funcNode.childForFieldName?.('expression')
1216
+ ?? null;
1217
+ }
1218
+ if (!innerReceiver && current.type === 'method_invocation') {
1219
+ innerReceiver = current.childForFieldName?.('object') ?? null;
1220
+ }
1221
+ if (!innerReceiver && (current.type === 'member_call_expression' || current.type === 'nullsafe_member_call_expression')) {
1222
+ innerReceiver = current.childForFieldName?.('object') ?? null;
1223
+ }
1224
+ if (!innerReceiver && current.type === 'call') {
1225
+ innerReceiver = current.childForFieldName?.('receiver') ?? null;
1226
+ }
1227
+ if (!innerReceiver)
1228
+ break;
1229
+ if (CALL_EXPRESSION_TYPES.has(innerReceiver.type) || FIELD_ACCESS_NODE_TYPES.has(innerReceiver.type)) {
1230
+ current = innerReceiver;
1231
+ }
1232
+ else {
1233
+ return { chain, baseReceiverName: innerReceiver.text || undefined };
1234
+ }
1235
+ }
1236
+ else if (FIELD_ACCESS_NODE_TYPES.has(current.type)) {
1237
+ // ── Field/member access: extract property name + inner object ─────────
1238
+ let propertyName;
1239
+ let innerObject = null;
1240
+ if (current.type === 'navigation_expression') {
1241
+ for (const child of current.children ?? []) {
1242
+ if (child.type === 'navigation_suffix') {
1243
+ for (const sc of child.children ?? []) {
1244
+ if (sc.isNamed && sc.type !== '.') {
1245
+ propertyName = sc.text;
1246
+ break;
1247
+ }
1248
+ }
1249
+ }
1250
+ else if (child.isNamed && !innerObject) {
1251
+ innerObject = child;
1252
+ }
1253
+ }
1254
+ }
1255
+ else if (current.type === 'attribute') {
1256
+ innerObject = current.childForFieldName?.('object') ?? null;
1257
+ propertyName = current.childForFieldName?.('attribute')?.text;
1258
+ }
1259
+ else {
1260
+ innerObject = current.childForFieldName?.('object')
1261
+ ?? current.childForFieldName?.('value')
1262
+ ?? current.childForFieldName?.('operand')
1263
+ ?? current.childForFieldName?.('argument') // C/C++ field_expression
1264
+ ?? current.childForFieldName?.('expression')
1265
+ ?? null;
1266
+ propertyName = (current.childForFieldName?.('property')
1267
+ ?? current.childForFieldName?.('field')
1268
+ ?? current.childForFieldName?.('name'))?.text;
1269
+ }
1270
+ if (!propertyName)
1271
+ break;
1272
+ chain.unshift({ kind: 'field', name: propertyName });
1273
+ if (!innerObject)
1274
+ break;
1275
+ if (CALL_EXPRESSION_TYPES.has(innerObject.type) || FIELD_ACCESS_NODE_TYPES.has(innerObject.type)) {
1276
+ current = innerObject;
1277
+ }
1278
+ else {
1279
+ return { chain, baseReceiverName: innerObject.text || undefined };
1280
+ }
1281
+ }
1282
+ else {
1283
+ // Simple identifier — this is the base receiver
1284
+ return chain.length > 0
1285
+ ? { chain, baseReceiverName: current.text || undefined }
1286
+ : undefined;
1287
+ }
1288
+ }
1289
+ return chain.length > 0 ? { chain, baseReceiverName: undefined } : undefined;
1290
+ }
@@ -1,5 +1,7 @@
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 { NodeLabel } from '../../graph/types.js';
3
5
  interface ParsedNode {
4
6
  id: string;
5
7
  label: string;
@@ -21,7 +23,7 @@ interface ParsedRelationship {
21
23
  id: string;
22
24
  sourceId: string;
23
25
  targetId: string;
24
- type: 'DEFINES' | 'HAS_METHOD';
26
+ type: 'DEFINES' | 'HAS_METHOD' | 'HAS_PROPERTY';
25
27
  confidence: number;
26
28
  reason: string;
27
29
  }
@@ -29,9 +31,10 @@ interface ParsedSymbol {
29
31
  filePath: string;
30
32
  name: string;
31
33
  nodeId: string;
32
- type: string;
34
+ type: NodeLabel;
33
35
  parameterCount?: number;
34
36
  returnType?: string;
37
+ declaredType?: string;
35
38
  ownerId?: string;
36
39
  }
37
40
  export interface ExtractedImport {
@@ -57,13 +60,25 @@ export interface ExtractedCall {
57
60
  /** Resolved type name of the receiver (e.g., 'User' for user.save() when user: User) */
58
61
  receiverTypeName?: string;
59
62
  /**
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'
63
+ * Unified mixed chain when the receiver is a chain of field accesses and/or method calls.
64
+ * Steps are ordered base-first (innermost to outermost). Examples:
65
+ * `svc.getUser().save()` → chain=[{kind:'call',name:'getUser'}], receiverName='svc'
66
+ * `user.address.save()` chain=[{kind:'field',name:'address'}], receiverName='user'
67
+ * `svc.getUser().address.save()` → chain=[{kind:'call',name:'getUser'},{kind:'field',name:'address'}]
64
68
  * Length is capped at MAX_CHAIN_DEPTH (3).
65
69
  */
66
- receiverCallChain?: string[];
70
+ receiverMixedChain?: MixedChainStep[];
71
+ }
72
+ export interface ExtractedAssignment {
73
+ filePath: string;
74
+ /** generateId of enclosing function, or generateId('File', filePath) for top-level */
75
+ sourceId: string;
76
+ /** Receiver text (e.g., 'user' from user.address = value) */
77
+ receiverText: string;
78
+ /** Property name being written (e.g., 'address') */
79
+ propertyName: string;
80
+ /** Resolved type name of the receiver if available from TypeEnv */
81
+ receiverTypeName?: string;
67
82
  }
68
83
  export interface ExtractedHeritage {
69
84
  filePath: string;
@@ -93,6 +108,7 @@ export interface ParseWorkerResult {
93
108
  symbols: ParsedSymbol[];
94
109
  imports: ExtractedImport[];
95
110
  calls: ExtractedCall[];
111
+ assignments: ExtractedAssignment[];
96
112
  heritage: ExtractedHeritage[];
97
113
  routes: ExtractedRoute[];
98
114
  constructorBindings: FileConstructorBindings[];
@@ -28,7 +28,7 @@ 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, 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';
@@ -37,6 +37,7 @@ import { generateId } from '../../../lib/utils.js';
37
37
  import { extractNamedBindings } from '../named-binding-extraction.js';
38
38
  import { appendKotlinWildcard } from '../resolvers/index.js';
39
39
  import { callRouters } from '../call-routing.js';
40
+ import { extractPropertyDeclaredType } from '../type-extractors/shared.js';
40
41
  // ============================================================================
41
42
  // Worker-local parser + language map
42
43
  // ============================================================================
@@ -162,6 +163,7 @@ const processBatch = (files, onProgress) => {
162
163
  symbols: [],
163
164
  imports: [],
164
165
  calls: [],
166
+ assignments: [],
165
167
  heritage: [],
166
168
  routes: [],
167
169
  constructorBindings: [],
@@ -759,6 +761,28 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
759
761
  });
760
762
  continue;
761
763
  }
764
+ // Extract assignment sites (field write access)
765
+ if (captureMap['assignment'] && captureMap['assignment.receiver'] && captureMap['assignment.property']) {
766
+ const receiverText = captureMap['assignment.receiver'].text;
767
+ const propertyName = captureMap['assignment.property'].text;
768
+ if (receiverText && propertyName) {
769
+ const srcId = findEnclosingFunctionId(captureMap['assignment'], file.path)
770
+ || generateId('File', file.path);
771
+ let receiverTypeName;
772
+ if (typeEnv) {
773
+ receiverTypeName = typeEnv.lookup(receiverText, captureMap['assignment']) ?? undefined;
774
+ }
775
+ result.assignments.push({
776
+ filePath: file.path,
777
+ sourceId: srcId,
778
+ receiverText,
779
+ propertyName,
780
+ ...(receiverTypeName ? { receiverTypeName } : {}),
781
+ });
782
+ }
783
+ if (!captureMap['call'])
784
+ continue;
785
+ }
762
786
  // Extract call sites
763
787
  if (captureMap['call']) {
764
788
  const callNameNode = captureMap['call.name'];
@@ -811,6 +835,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
811
835
  nodeId,
812
836
  type: 'Property',
813
837
  ...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
838
+ ...(item.declaredType ? { declaredType: item.declaredType } : {}),
814
839
  });
815
840
  const fileId = generateId('File', file.path);
816
841
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
@@ -824,10 +849,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
824
849
  });
825
850
  if (propEnclosingClassId) {
826
851
  result.relationships.push({
827
- id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
852
+ id: generateId('HAS_PROPERTY', `${propEnclosingClassId}->${nodeId}`),
828
853
  sourceId: propEnclosingClassId,
829
854
  targetId: nodeId,
830
- type: 'HAS_METHOD',
855
+ type: 'HAS_PROPERTY',
831
856
  confidence: 1.0,
832
857
  reason: '',
833
858
  });
@@ -844,26 +869,19 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
844
869
  const callForm = inferCallForm(callNode, callNameNode);
845
870
  let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
846
871
  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.
872
+ let receiverMixedChain;
873
+ // When the receiver is a complex expression (call chain, field chain, or mixed),
874
+ // extractReceiverName returns undefined. Walk the receiver node to build a unified
875
+ // mixed chain for deferred resolution in processCallsFromExtracted.
855
876
  if (callForm === 'member' && receiverName === undefined && !receiverTypeName) {
856
877
  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.
878
+ if (receiverNode) {
879
+ const extracted = extractMixedChain(receiverNode);
880
+ if (extracted && extracted.chain.length > 0) {
881
+ receiverMixedChain = extracted.chain;
863
882
  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.
883
+ // Try the type environment immediately for the base receiver
884
+ // (covers explicitly-typed locals and annotated parameters).
867
885
  if (receiverName) {
868
886
  receiverTypeName = typeEnv.lookup(receiverName, callNode);
869
887
  }
@@ -878,7 +896,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
878
896
  ...(callForm !== undefined ? { callForm } : {}),
879
897
  ...(receiverName !== undefined ? { receiverName } : {}),
880
898
  ...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
881
- ...(receiverCallChain !== undefined ? { receiverCallChain } : {}),
899
+ ...(receiverMixedChain !== undefined ? { receiverMixedChain } : {}),
882
900
  });
883
901
  }
884
902
  }
@@ -926,6 +944,21 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
926
944
  const nodeLabel = getLabelFromCaptures(captureMap);
927
945
  if (!nodeLabel)
928
946
  continue;
947
+ // C/C++: @definition.function is broad and also matches inline class methods (inside
948
+ // a class/struct body). Those are already captured by @definition.method, so skip
949
+ // the duplicate Function entry to prevent double-indexing in globalIndex.
950
+ if ((language === SupportedLanguages.CPlusPlus || language === SupportedLanguages.C) &&
951
+ nodeLabel === 'Function') {
952
+ let ancestor = captureMap['definition.function']?.parent;
953
+ while (ancestor) {
954
+ if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') {
955
+ break; // inside a class body — duplicate of @definition.method
956
+ }
957
+ ancestor = ancestor.parent;
958
+ }
959
+ if (ancestor)
960
+ continue; // found a class/struct ancestor → skip
961
+ }
929
962
  const nameNode = captureMap['name'];
930
963
  // Synthesize name for constructors without explicit @name capture (e.g. Swift init)
931
964
  if (!nameNode && nodeLabel !== 'Constructor')
@@ -948,6 +981,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
948
981
  : null;
949
982
  let parameterCount;
950
983
  let returnType;
984
+ let declaredType;
951
985
  if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
952
986
  const sig = extractMethodSignature(definitionNode);
953
987
  parameterCount = sig.parameterCount;
@@ -963,6 +997,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
963
997
  }
964
998
  }
965
999
  }
1000
+ else if (nodeLabel === 'Property' && definitionNode) {
1001
+ // Extract the declared type for property/field nodes.
1002
+ // Walk the definition node for type annotation children.
1003
+ declaredType = extractPropertyDeclaredType(definitionNode);
1004
+ }
966
1005
  result.nodes.push({
967
1006
  id: nodeId,
968
1007
  label: nodeLabel,
@@ -993,6 +1032,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
993
1032
  type: nodeLabel,
994
1033
  ...(parameterCount !== undefined ? { parameterCount } : {}),
995
1034
  ...(returnType !== undefined ? { returnType } : {}),
1035
+ ...(declaredType !== undefined ? { declaredType } : {}),
996
1036
  ...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
997
1037
  });
998
1038
  const fileId = generateId('File', file.path);
@@ -1005,13 +1045,14 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1005
1045
  confidence: 1.0,
1006
1046
  reason: '',
1007
1047
  });
1008
- // ── HAS_METHOD: link method/constructor/property to enclosing class ──
1048
+ // ── HAS_METHOD / HAS_PROPERTY: link member to enclosing class ──
1009
1049
  if (enclosingClassId) {
1050
+ const memberEdgeType = nodeLabel === 'Property' ? 'HAS_PROPERTY' : 'HAS_METHOD';
1010
1051
  result.relationships.push({
1011
- id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
1052
+ id: generateId(memberEdgeType, `${enclosingClassId}->${nodeId}`),
1012
1053
  sourceId: enclosingClassId,
1013
1054
  targetId: nodeId,
1014
- type: 'HAS_METHOD',
1055
+ type: memberEdgeType,
1015
1056
  confidence: 1.0,
1016
1057
  reason: '',
1017
1058
  });
@@ -1030,7 +1071,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1030
1071
  /** Accumulated result across sub-batches */
1031
1072
  let accumulated = {
1032
1073
  nodes: [], relationships: [], symbols: [],
1033
- imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
1074
+ imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
1034
1075
  };
1035
1076
  let cumulativeProcessed = 0;
1036
1077
  const mergeResult = (target, src) => {
@@ -1039,6 +1080,7 @@ const mergeResult = (target, src) => {
1039
1080
  target.symbols.push(...src.symbols);
1040
1081
  target.imports.push(...src.imports);
1041
1082
  target.calls.push(...src.calls);
1083
+ target.assignments.push(...src.assignments);
1042
1084
  target.heritage.push(...src.heritage);
1043
1085
  target.routes.push(...src.routes);
1044
1086
  target.constructorBindings.push(...src.constructorBindings);
@@ -1064,7 +1106,7 @@ parentPort.on('message', (msg) => {
1064
1106
  if (msg && msg.type === 'flush') {
1065
1107
  parentPort.postMessage({ type: 'result', data: accumulated });
1066
1108
  // Reset for potential reuse
1067
- accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
1109
+ accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
1068
1110
  cumulativeProcessed = 0;
1069
1111
  return;
1070
1112
  }
@@ -1,5 +1,7 @@
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;
3
5
  export declare const initLbug: (dbPath: string) => Promise<{
4
6
  db: lbug.Database;
5
7
  conn: lbug.Connection;
@@ -9,6 +9,8 @@ let db = null;
9
9
  let conn = null;
10
10
  let currentDbPath = null;
11
11
  let ftsLoaded = false;
12
+ /** Expose the current Database for pool adapter reuse in tests. */
13
+ export const getDatabase = () => db;
12
14
  // Global session lock for operations that touch module-level lbug globals.
13
15
  // This guarantees no DB switch can happen while an operation is running.
14
16
  let sessionLock = Promise.resolve();
@@ -11,7 +11,7 @@
11
11
  export declare const NODE_TABLES: readonly ["File", "Folder", "Function", "Class", "Interface", "Method", "CodeElement", "Community", "Process", "Struct", "Enum", "Macro", "Typedef", "Union", "Namespace", "Trait", "Impl", "TypeAlias", "Const", "Static", "Property", "Record", "Delegate", "Annotation", "Constructor", "Template", "Module"];
12
12
  export type NodeTableName = typeof NODE_TABLES[number];
13
13
  export declare const REL_TABLE_NAME = "CodeRelation";
14
- export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "HAS_METHOD", "OVERRIDES", "MEMBER_OF", "STEP_IN_PROCESS"];
14
+ export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "HAS_METHOD", "HAS_PROPERTY", "ACCESSES", "OVERRIDES", "MEMBER_OF", "STEP_IN_PROCESS"];
15
15
  export type RelType = typeof REL_TYPES[number];
16
16
  export declare const EMBEDDING_TABLE_NAME = "CodeEmbedding";
17
17
  export declare const FILE_SCHEMA = "\nCREATE NODE TABLE File (\n id STRING,\n name STRING,\n filePath STRING,\n content STRING,\n PRIMARY KEY (id)\n)";
@@ -22,7 +22,7 @@ export const NODE_TABLES = [
22
22
  // ============================================================================
23
23
  export const REL_TABLE_NAME = 'CodeRelation';
24
24
  // Valid relation types
25
- export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'];
25
+ export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'];
26
26
  // ============================================================================
27
27
  // EMBEDDING TABLE
28
28
  // ============================================================================
@@ -12,11 +12,29 @@
12
12
  * @see https://docs.ladybugdb.com/concurrency — multiple Connections
13
13
  * from the same Database is the officially supported concurrency pattern.
14
14
  */
15
+ import lbug from '@ladybugdb/core';
16
+ /** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
17
+ export declare const realStdoutWrite: any;
15
18
  /**
16
19
  * Initialize (or reuse) a Database + connection pool for a specific repo.
17
20
  * Retries on lock errors (e.g., when `gitnexus analyze` is running).
21
+ *
22
+ * Concurrent calls for the same repoId are deduplicated — the second caller
23
+ * awaits the first's in-progress init rather than starting a redundant one.
18
24
  */
19
25
  export declare const initLbug: (repoId: string, dbPath: string) => Promise<void>;
26
+ /**
27
+ * Initialize a pool entry from a pre-existing Database object.
28
+ *
29
+ * Used in tests to avoid the writable→close→read-only cycle that crashes
30
+ * on macOS due to N-API destructor segfaults. The pool adapter reuses
31
+ * the core adapter's writable Database instead of opening a new read-only one.
32
+ *
33
+ * The Database is registered in the shared dbCache so closeOne() decrements
34
+ * the refCount correctly. If the Database is already cached (e.g. another
35
+ * repoId already injected it), the existing entry is reused.
36
+ */
37
+ export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
20
38
  export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
21
39
  /**
22
40
  * Execute a parameterized query on a specific repo's connection pool.
@@ -33,3 +51,7 @@ export declare const closeLbug: (repoId?: string) => Promise<void>;
33
51
  * Check if a specific repo's pool is active
34
52
  */
35
53
  export declare const isLbugReady: (repoId: string) => boolean;
54
+ /** Regex to detect write operations in user-supplied Cypher queries */
55
+ export declare const CYPHER_WRITE_RE: RegExp;
56
+ /** Check if a Cypher query contains write operations */
57
+ export declare function isWriteQuery(query: string): boolean;