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
@@ -22,12 +22,12 @@ const MAX_POOL_SIZE = 5;
22
22
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
23
23
  /** Max connections per repo (caps concurrent queries per repo) */
24
24
  const MAX_CONNS_PER_REPO = 8;
25
- /** Connections created eagerly on init */
26
- const INITIAL_CONNS_PER_REPO = 2;
27
25
  let idleTimer = null;
28
26
  /** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
29
- const realStdoutWrite = process.stdout.write.bind(process.stdout);
27
+ export const realStdoutWrite = process.stdout.write.bind(process.stdout);
30
28
  let stdoutSilenceCount = 0;
29
+ /** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
30
+ let preWarmActive = false;
31
31
  /**
32
32
  * Start the idle cleanup timer (runs every 60s)
33
33
  */
@@ -65,19 +65,42 @@ function evictLRU() {
65
65
  }
66
66
  }
67
67
  /**
68
- * Remove a repo from the pool and release its shared Database ref.
69
- *
70
- * LadybugDB's native .closeSync() triggers N-API destructor hooks that
71
- * segfault on Linux/macOS. Pool databases are opened read-only, so
72
- * there is no WAL to flush — just deleting the pool entry and letting
73
- * the GC (or process exit) reclaim native resources is safe.
68
+ * Remove a repo from the pool, close its connections, and release its
69
+ * shared Database ref. Only closes the Database when no other repoIds
70
+ * reference it (refCount === 0).
74
71
  */
75
72
  function closeOne(repoId) {
76
73
  const entry = pool.get(repoId);
77
- if (entry) {
78
- const shared = dbCache.get(entry.dbPath);
79
- if (shared && shared.refCount > 0) {
80
- shared.refCount--;
74
+ if (!entry)
75
+ return;
76
+ entry.closed = true;
77
+ // Close available connections — fire-and-forget with .catch() to prevent
78
+ // unhandled rejections. Native close() returns Promise<void> but can crash
79
+ // the N-API destructor on macOS/Windows; deferring to process exit lets
80
+ // dangerouslyIgnoreUnhandledErrors absorb the crash.
81
+ for (const conn of entry.available) {
82
+ conn.close().catch(() => { });
83
+ }
84
+ entry.available.length = 0;
85
+ // Checked-out connections can't be closed here — they're in-flight.
86
+ // The checkin() function detects entry.closed and closes them on return.
87
+ // Only close the Database when no other repoIds reference it.
88
+ // External databases (injected via initLbugWithDb) are never closed here —
89
+ // the core adapter owns them and handles their lifecycle.
90
+ const shared = dbCache.get(entry.dbPath);
91
+ if (shared) {
92
+ shared.refCount--;
93
+ if (shared.refCount === 0) {
94
+ if (shared.external) {
95
+ // External databases are owned by the core adapter — don't close
96
+ // or remove from cache. Keep the entry so future initLbug() calls
97
+ // for the same dbPath reuse it instead of hitting a file lock.
98
+ shared.refCount = 0;
99
+ }
100
+ else {
101
+ shared.db.close().catch(() => { });
102
+ dbCache.delete(entry.dbPath);
103
+ }
81
104
  }
82
105
  }
83
106
  pool.delete(repoId);
@@ -97,6 +120,14 @@ function restoreStdout() {
97
120
  process.stdout.write = realStdoutWrite;
98
121
  }
99
122
  }
123
+ // Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
124
+ // inside createConnection before restoreStdout runs).
125
+ setInterval(() => {
126
+ if (stdoutSilenceCount > 0 && !preWarmActive) {
127
+ stdoutSilenceCount = 0;
128
+ process.stdout.write = realStdoutWrite;
129
+ }
130
+ }, 1000).unref();
100
131
  function createConnection(db) {
101
132
  silenceStdout();
102
133
  try {
@@ -112,9 +143,14 @@ const QUERY_TIMEOUT_MS = 30_000;
112
143
  const WAITER_TIMEOUT_MS = 15_000;
113
144
  const LOCK_RETRY_ATTEMPTS = 3;
114
145
  const LOCK_RETRY_DELAY_MS = 2000;
146
+ /** Deduplicates concurrent initLbug calls for the same repoId */
147
+ const initPromises = new Map();
115
148
  /**
116
149
  * Initialize (or reuse) a Database + connection pool for a specific repo.
117
150
  * Retries on lock errors (e.g., when `gitnexus analyze` is running).
151
+ *
152
+ * Concurrent calls for the same repoId are deduplicated — the second caller
153
+ * awaits the first's in-progress init rather than starting a redundant one.
118
154
  */
119
155
  export const initLbug = async (repoId, dbPath) => {
120
156
  const existing = pool.get(repoId);
@@ -122,6 +158,27 @@ export const initLbug = async (repoId, dbPath) => {
122
158
  existing.lastUsed = Date.now();
123
159
  return;
124
160
  }
161
+ // Deduplicate concurrent init calls for the same repoId —
162
+ // prevents double-init race when multiple parallel tool calls
163
+ // trigger initialization for the same repo simultaneously.
164
+ const pending = initPromises.get(repoId);
165
+ if (pending)
166
+ return pending;
167
+ const promise = doInitLbug(repoId, dbPath);
168
+ initPromises.set(repoId, promise);
169
+ try {
170
+ await promise;
171
+ }
172
+ finally {
173
+ initPromises.delete(repoId);
174
+ }
175
+ };
176
+ /**
177
+ * Internal init — creates DB, pre-warms connections, loads FTS, then registers pool.
178
+ * Pool entry is registered LAST so concurrent executeQuery calls see either
179
+ * "not initialized" (and throw) or a fully ready pool — never a half-built one.
180
+ */
181
+ async function doInitLbug(repoId, dbPath) {
125
182
  // Check if database exists
126
183
  try {
127
184
  await fs.stat(dbPath);
@@ -166,14 +223,22 @@ export const initLbug = async (repoId, dbPath) => {
166
223
  }
167
224
  shared.refCount++;
168
225
  const db = shared.db;
169
- // Pre-create a small pool of connections
226
+ // Pre-create the full pool upfront so createConnection() (which silences
227
+ // stdout) is never called lazily during active query execution.
228
+ // Mark preWarmActive so the watchdog timer doesn't interfere.
229
+ preWarmActive = true;
170
230
  const available = [];
171
- for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
172
- available.push(createConnection(db));
231
+ try {
232
+ for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
233
+ available.push(createConnection(db));
234
+ }
173
235
  }
174
- pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
175
- ensureIdleTimer();
176
- // Load FTS extension once per shared Database
236
+ finally {
237
+ preWarmActive = false;
238
+ }
239
+ // Load FTS extension once per shared Database.
240
+ // Done BEFORE pool registration so no concurrent checkout can grab
241
+ // the connection while the async FTS load is in progress.
177
242
  if (!shared.ftsLoaded) {
178
243
  try {
179
244
  await available[0].query('LOAD EXTENSION fts');
@@ -183,7 +248,67 @@ export const initLbug = async (repoId, dbPath) => {
183
248
  // Extension may not be installed — FTS queries will fail gracefully
184
249
  }
185
250
  }
186
- };
251
+ // Register pool entry only after all connections are pre-warmed and FTS is
252
+ // loaded. Concurrent executeQuery calls see either "not initialized"
253
+ // (and throw cleanly) or a fully ready pool — never a half-built one.
254
+ pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath, closed: false });
255
+ ensureIdleTimer();
256
+ }
257
+ /**
258
+ * Initialize a pool entry from a pre-existing Database object.
259
+ *
260
+ * Used in tests to avoid the writable→close→read-only cycle that crashes
261
+ * on macOS due to N-API destructor segfaults. The pool adapter reuses
262
+ * the core adapter's writable Database instead of opening a new read-only one.
263
+ *
264
+ * The Database is registered in the shared dbCache so closeOne() decrements
265
+ * the refCount correctly. If the Database is already cached (e.g. another
266
+ * repoId already injected it), the existing entry is reused.
267
+ */
268
+ export async function initLbugWithDb(repoId, existingDb, dbPath) {
269
+ const existing = pool.get(repoId);
270
+ if (existing) {
271
+ existing.lastUsed = Date.now();
272
+ return;
273
+ }
274
+ // Register in dbCache with external: true so other initLbug() calls
275
+ // for the same dbPath reuse this Database instead of trying to open
276
+ // a new one (which would fail with a file lock error).
277
+ // closeOne() respects the external flag and skips db.close().
278
+ let shared = dbCache.get(dbPath);
279
+ if (!shared) {
280
+ shared = { db: existingDb, refCount: 0, ftsLoaded: false, external: true };
281
+ dbCache.set(dbPath, shared);
282
+ }
283
+ shared.refCount++;
284
+ const available = [];
285
+ preWarmActive = true;
286
+ try {
287
+ for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
288
+ available.push(createConnection(existingDb));
289
+ }
290
+ }
291
+ finally {
292
+ preWarmActive = false;
293
+ }
294
+ // Load FTS extension if not already loaded on this Database
295
+ try {
296
+ await available[0].query('LOAD EXTENSION fts');
297
+ }
298
+ catch {
299
+ // Extension may already be loaded or not installed
300
+ }
301
+ pool.set(repoId, {
302
+ db: existingDb,
303
+ available,
304
+ checkedOut: 0,
305
+ waiters: [],
306
+ lastUsed: Date.now(),
307
+ dbPath,
308
+ closed: false
309
+ });
310
+ ensureIdleTimer();
311
+ }
187
312
  /**
188
313
  * Checkout a connection from the pool.
189
314
  * Returns an available connection, or creates a new one if under the cap.
@@ -195,11 +320,14 @@ function checkout(entry) {
195
320
  entry.checkedOut++;
196
321
  return Promise.resolve(entry.available.pop());
197
322
  }
198
- // Grow the pool if under the cap
323
+ // Pool was pre-warmed to MAX_CONNS_PER_REPO during init. If we're here
324
+ // with fewer total connections, something leaked — surface the bug rather
325
+ // than silently creating a connection (which would silence stdout mid-query).
199
326
  const totalConns = entry.available.length + entry.checkedOut;
200
327
  if (totalConns < MAX_CONNS_PER_REPO) {
201
- entry.checkedOut++;
202
- return Promise.resolve(createConnection(entry.db));
328
+ throw new Error(`Connection pool integrity error: expected ${MAX_CONNS_PER_REPO} ` +
329
+ `connections but found ${totalConns} (${entry.available.length} available, ` +
330
+ `${entry.checkedOut} checked out)`);
203
331
  }
204
332
  // At capacity — queue the caller with a timeout.
205
333
  return new Promise((resolve, reject) => {
@@ -218,10 +346,17 @@ function checkout(entry) {
218
346
  }
219
347
  /**
220
348
  * Return a connection to the pool after use.
349
+ * If the pool entry was closed while the connection was checked out (e.g.
350
+ * LRU eviction), close the orphaned connection instead of returning it.
221
351
  * If there are queued waiters, hand the connection directly to the next one
222
352
  * instead of putting it back in the available array (avoids race conditions).
223
353
  */
224
354
  function checkin(entry, conn) {
355
+ if (entry.closed) {
356
+ // Pool entry was deleted during checkout — close the orphaned connection
357
+ conn.close().catch(() => { });
358
+ return;
359
+ }
225
360
  if (entry.waiters.length > 0) {
226
361
  // Hand directly to the next waiter — no intermediate available state
227
362
  const waiter = entry.waiters.shift();
@@ -249,6 +384,9 @@ export const executeQuery = async (repoId, cypher) => {
249
384
  if (!entry) {
250
385
  throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
251
386
  }
387
+ if (isWriteQuery(cypher)) {
388
+ throw new Error('Write operations are not allowed. The pool adapter is read-only.');
389
+ }
252
390
  entry.lastUsed = Date.now();
253
391
  const conn = await checkout(entry);
254
392
  try {
@@ -309,3 +447,9 @@ export const closeLbug = async (repoId) => {
309
447
  * Check if a specific repo's pool is active
310
448
  */
311
449
  export const isLbugReady = (repoId) => pool.has(repoId);
450
+ /** Regex to detect write operations in user-supplied Cypher queries */
451
+ export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
452
+ /** Check if a Cypher query contains write operations */
453
+ export function isWriteQuery(query) {
454
+ return CYPHER_WRITE_RE.test(query);
455
+ }
@@ -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', 'HAS_METHOD', 'OVERRIDES']);
40
+ export const VALID_RELATION_TYPES = new Set(['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']);
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 */
@@ -788,14 +788,14 @@ export class LocalBackend {
788
788
  // Categorized incoming refs
789
789
  const incomingRows = await executeParameterized(repo.id, `
790
790
  MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
791
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
791
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
792
792
  RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
793
793
  LIMIT 30
794
794
  `, { symId });
795
795
  // Categorized outgoing refs
796
796
  const outgoingRows = await executeParameterized(repo.id, `
797
797
  MATCH (n {id: $symId})-[r:CodeRelation]->(target)
798
- WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
798
+ WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'OVERRIDES', 'ACCESSES']
799
799
  RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
800
800
  LIMIT 30
801
801
  `, { symId });
@@ -271,6 +271,13 @@ nodes:
271
271
 
272
272
  additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
273
273
 
274
+ node_properties:
275
+ common: "name (STRING), filePath (STRING), startLine (INT32), endLine (INT32)"
276
+ Method: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL)"
277
+ Function: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL)"
278
+ Property: "declaredType (STRING) — the field's type annotation (e.g., 'Address', 'City'). Used for field-access chain resolution."
279
+ Constructor: "parameterCount (INT32)"
280
+
274
281
  relationships:
275
282
  - CONTAINS: File/Folder contains child
276
283
  - DEFINES: File defines a symbol
@@ -278,6 +285,10 @@ relationships:
278
285
  - IMPORTS: Module imports
279
286
  - EXTENDS: Class inheritance
280
287
  - IMPLEMENTS: Interface implementation
288
+ - HAS_METHOD: Class/Struct/Interface owns a Method
289
+ - HAS_PROPERTY: Class/Struct/Interface owns a Property (field)
290
+ - ACCESSES: Function/Method reads or writes a Property (reason: 'read' or 'write')
291
+ - OVERRIDES: Method overrides another Method (MRO)
281
292
  - MEMBER_OF: Symbol belongs to community
282
293
  - STEP_IN_PROCESS: Symbol is step N in process
283
294
 
@@ -15,6 +15,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
15
  import { CompatibleStdioServerTransport } from './compatible-stdio-transport.js';
16
16
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
17
17
  import { GITNEXUS_TOOLS } from './tools.js';
18
+ import { realStdoutWrite } from './core/lbug-adapter.js';
18
19
  import { getResourceDefinitions, getResourceTemplates, readResource } from './resources.js';
19
20
  /**
20
21
  * Next-step hints appended to tool responses.
@@ -237,12 +238,22 @@ Follow these steps:
237
238
  */
238
239
  export async function startMCPServer(backend) {
239
240
  const server = createMCPServer(backend);
240
- // Connect to stdio transport
241
- const transport = new CompatibleStdioServerTransport();
241
+ // Use the shared stdout reference captured at module-load time by the
242
+ // lbug-adapter. Avoids divergence if anything patches stdout between
243
+ // module load and server start.
244
+ const _safeStdout = new Proxy(process.stdout, {
245
+ get(target, prop, receiver) {
246
+ if (prop === 'write')
247
+ return realStdoutWrite;
248
+ const val = Reflect.get(target, prop, receiver);
249
+ return typeof val === 'function' ? val.bind(target) : val;
250
+ }
251
+ });
252
+ const transport = new CompatibleStdioServerTransport(process.stdin, _safeStdout);
242
253
  await server.connect(transport);
243
254
  // Graceful shutdown helper
244
255
  let shuttingDown = false;
245
- const shutdown = async () => {
256
+ const shutdown = async (exitCode = 0) => {
246
257
  if (shuttingDown)
247
258
  return;
248
259
  shuttingDown = true;
@@ -254,11 +265,22 @@ export async function startMCPServer(backend) {
254
265
  await server.close();
255
266
  }
256
267
  catch { }
257
- process.exit(0);
268
+ process.exit(exitCode);
258
269
  };
259
270
  // Handle graceful shutdown
260
271
  process.on('SIGINT', shutdown);
261
272
  process.on('SIGTERM', shutdown);
273
+ // Log crashes to stderr so they aren't silently lost.
274
+ // uncaughtException is fatal — shut down.
275
+ // unhandledRejection is logged but kept non-fatal (availability-first):
276
+ // killing the server for one missed catch would be worse than logging it.
277
+ process.on('uncaughtException', (err) => {
278
+ process.stderr.write(`GitNexus MCP uncaughtException: ${err?.stack || err}\n`);
279
+ shutdown(1);
280
+ });
281
+ process.on('unhandledRejection', (reason) => {
282
+ process.stderr.write(`GitNexus MCP unhandledRejection: ${reason?.stack || reason}\n`);
283
+ });
262
284
  // Handle stdio errors — stdin close means the parent process is gone
263
285
  process.stdin.on('end', shutdown);
264
286
  process.stdin.on('error', () => shutdown());
package/dist/mcp/tools.js CHANGED
@@ -61,7 +61,7 @@ SCHEMA:
61
61
  - Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
62
62
  - Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
63
63
  - All edges via single CodeRelation table with 'type' property
64
- - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
64
+ - Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, ACCESSES, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
65
65
  - Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
66
66
 
67
67
  EXAMPLES:
@@ -77,6 +77,12 @@ EXAMPLES:
77
77
  • Find all methods of a class:
78
78
  MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
79
79
 
80
+ • Find all properties of a class:
81
+ MATCH (c:Class {name: "User"})-[r:CodeRelation {type: 'HAS_PROPERTY'}]->(p:Property) RETURN p.name, p.declaredType
82
+
83
+ • Find all writers of a field:
84
+ MATCH (f:Function)-[r:CodeRelation {type: 'ACCESSES', reason: 'write'}]->(p:Property) WHERE p.name = "address" RETURN f.name, f.filePath
85
+
80
86
  • Find method overrides (MRO resolution):
81
87
  MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
82
88
 
@@ -102,12 +108,14 @@ TIPS:
102
108
  {
103
109
  name: 'context',
104
110
  description: `360-degree view of a single code symbol.
105
- Shows categorized incoming/outgoing references (calls, imports, extends, implements), process participation, and file location.
111
+ Shows categorized incoming/outgoing references (calls, imports, extends, implements, methods, properties, overrides), process participation, and file location.
106
112
 
107
113
  WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
108
114
  AFTER THIS: Use impact() if planning changes, or READ gitnexus://repo/{name}/process/{processName} for full execution trace.
109
115
 
110
- Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.`,
116
+ Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.
117
+
118
+ NOTE: ACCESSES edges (field read/write tracking) are included in context results with reason 'read' or 'write'. CALLS edges resolve through field access chains and method-call chains (e.g., user.address.getCity().save() produces CALLS edges at each step).`,
111
119
  inputSchema: {
112
120
  type: 'object',
113
121
  properties: {
@@ -183,7 +191,9 @@ Depth groups:
183
191
  - d=2: LIKELY AFFECTED (indirect)
184
192
  - d=3: MAY NEED TESTING (transitive)
185
193
 
186
- EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES
194
+ TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes.
195
+
196
+ EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES
187
197
  Confidence: 1.0 = certain, <0.8 = fuzzy match`,
188
198
  inputSchema: {
189
199
  type: 'object',
@@ -191,7 +201,7 @@ Confidence: 1.0 = certain, <0.8 = fuzzy match`,
191
201
  target: { type: 'string', description: 'Name of function, class, or file to analyze' },
192
202
  direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
193
203
  maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
194
- relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES (default: usage-based)' },
204
+ relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, HAS_PROPERTY, OVERRIDES, ACCESSES (default: usage-based, ACCESSES excluded by default)' },
195
205
  includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
196
206
  minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
197
207
  repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
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",
@@ -39,9 +39,9 @@
39
39
  "scripts": {
40
40
  "build": "tsc",
41
41
  "dev": "tsx watch src/cli/index.ts",
42
- "test": "vitest run test/unit",
42
+ "test": "vitest run",
43
+ "test:unit": "vitest run test/unit",
43
44
  "test:integration": "vitest run test/integration",
44
- "test:all": "vitest run",
45
45
  "test:watch": "vitest",
46
46
  "test:coverage": "vitest run --coverage",
47
47
  "prepare": "npm run build",
@@ -59,7 +59,7 @@
59
59
  "graphology": "^0.25.4",
60
60
  "graphology-indices": "^0.17.0",
61
61
  "graphology-utils": "^2.3.0",
62
- "@ladybugdb/core": "^0.15.1",
62
+ "@ladybugdb/core": "^0.15.2",
63
63
  "ignore": "^7.0.5",
64
64
  "lru-cache": "^11.0.0",
65
65
  "mnemonist": "^0.39.0",