gitnexus 1.6.10-rc.47 → 1.6.10-rc.49

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.
package/README.md CHANGED
@@ -471,6 +471,8 @@ Configure the behavior with these environment variables:
471
471
  | `GITNEXUS_FTS_CJK_SEGMENTATION` | `none`, `bigram` | `none` | `bigram` inserts overlapping character-bigram boundaries into Chinese/Japanese Han-ideograph spans in `content`/`description` before FTS indexing, so LadybugDB's space-only tokenizer can see sub-phrase word boundaries. Scoped to CJK Unified Ideographs only — Japanese Hiragana/Katakana and Korean Hangul are not currently segmented. Unlike `GITNEXUS_FTS_STEMMER`, this rewrites stored text — enabling it on an already-indexed repo requires a full `gitnexus analyze --force`; neither `--repair-fts` nor a plain incremental `analyze` applies it to previously-indexed files. Set the same value wherever `analyze` and search-serving processes (CLI query, MCP server, web server) run. |
472
472
  | `GITNEXUS_COMMUNITY_ENGINE` | `graphology`, `icebug`, `auto` | `graphology` | Community-detection engine used during analyze. `graphology` uses the bundled default path. `icebug` and `auto` currently behave identically: both try the experimental Icebug CSR path and fall back to Graphology if the optional native module is unavailable or incompatible. |
473
473
  | `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
474
+ | `GITNEXUS_LBUG_BUFFER_POOL_SIZE` | integer `>= 0` (bytes) | min(2 GiB, 80% RAM) | LadybugDB buffer-pool ceiling for every GitNexus database (analyze, MCP server, serve, group bridges). Bounded so a long-lived `gitnexus mcp` process or a large incremental `analyze` cannot grow toward LadybugDB's native 80%-of-RAM default and OOM the host (#2557). `0` restores that native unbounded default; invalid values warn and fall back to the default. |
475
+ | `GITNEXUS_LBUG_MAX_DB_SIZE` | positive integer (bytes) | `17179869184` (16 GiB) | Upper bound for a single LadybugDB database file. This is an mmap/disk-address-space ceiling, not a memory limit — it does not constrain the buffer pool (use `GITNEXUS_LBUG_BUFFER_POOL_SIZE` for that). Raise it when indexing genuinely huge monorepos; invalid values silently fall back to the default. |
474
476
 
475
477
  ```bash
476
478
  # Offline/airgapped: never reach the network for extensions
@@ -36,6 +36,7 @@ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
36
36
  import { encodeMarker } from '../../utils/heritage-marker.js';
37
37
  import { DART_BUILT_INS } from './built-ins.js';
38
38
  import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
39
+ import { preprocessDartExtensionTypes } from './extension-type-preprocess.js';
39
40
  const FUNCTION_DECL_TAGS = [
40
41
  '@declaration.function',
41
42
  '@declaration.method',
@@ -70,14 +71,15 @@ function dartLexicalFunctionOwner(input) {
70
71
  return undefined;
71
72
  }
72
73
  export function emitDartScopeCaptures(sourceText, _filePath, cachedTree) {
74
+ const parseText = preprocessDartExtensionTypes(sourceText);
73
75
  let tree;
74
76
  if (cachedTree !== undefined && cachedTree !== null) {
75
77
  tree = cachedTree;
76
78
  recordCacheHit();
77
79
  }
78
80
  else {
79
- tree = parseSourceSafe(getDartParser(), sourceText, undefined, {
80
- bufferSize: getTreeSitterBufferSize(sourceText),
81
+ tree = parseSourceSafe(getDartParser(), parseText, undefined, {
82
+ bufferSize: getTreeSitterBufferSize(parseText),
81
83
  });
82
84
  recordCacheMiss();
83
85
  }
@@ -161,6 +163,10 @@ export function emitDartScopeCaptures(sourceText, _filePath, cachedTree) {
161
163
  emitHeritage(node, out);
162
164
  return;
163
165
  }
166
+ if (node.type === 'extension_declaration') {
167
+ emitExtensionImplementsHeritage(node, out);
168
+ return;
169
+ }
164
170
  });
165
171
  out.push(...synthesizeCallableFlowCaptures(root, DART_CALLABLE_CAPTURE_OPTIONS));
166
172
  return out;
@@ -475,6 +481,47 @@ function emitHeritage(classNode, out) {
475
481
  emitHeritageMarkers(interfaces, 'implements', className, out);
476
482
  }
477
483
  }
484
+ function emitExtensionImplementsHeritage(extensionNode, out) {
485
+ const nameNode = extensionNode.childForFieldName('name');
486
+ if (nameNode === null)
487
+ return;
488
+ const bodyStart = extensionNode.text.indexOf('{');
489
+ const header = bodyStart === -1 ? extensionNode.text : extensionNode.text.slice(0, bodyStart);
490
+ const implementsIndex = header.indexOf('implements');
491
+ if (implementsIndex === -1)
492
+ return;
493
+ const className = nameNode.text;
494
+ const interfaces = header.slice(implementsIndex + 'implements'.length);
495
+ for (const rawInterface of splitTopLevelCommaList(interfaces)) {
496
+ const target = /^[ \t]*([A-Za-z_$][A-Za-z0-9_$]*)/.exec(rawInterface)?.[1];
497
+ if (target === undefined)
498
+ continue;
499
+ const payload = encodeMarker('heritage', ['implements', target, className]);
500
+ out.push({ '@import.heritage': syntheticCapture('@import.heritage', nameNode, payload) });
501
+ }
502
+ }
503
+ function splitTopLevelCommaList(text) {
504
+ const parts = [];
505
+ let start = 0;
506
+ let angleDepth = 0;
507
+ for (let i = 0; i < text.length; i++) {
508
+ const ch = text[i];
509
+ if (ch === '<') {
510
+ angleDepth++;
511
+ continue;
512
+ }
513
+ if (ch === '>' && angleDepth > 0) {
514
+ angleDepth--;
515
+ continue;
516
+ }
517
+ if (ch === ',' && angleDepth === 0) {
518
+ parts.push(text.slice(start, i));
519
+ start = i + 1;
520
+ }
521
+ }
522
+ parts.push(text.slice(start));
523
+ return parts;
524
+ }
478
525
  function emitHeritageMarkers(container, kind, className, out) {
479
526
  for (let i = 0; i < container.namedChildCount; i++) {
480
527
  const c = container.namedChild(i);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Rewrites Dart 3.3 `extension type Name(Type value)` headers into ordinary
3
+ * extension headers before tree-sitter sees the source. The vendored grammar
4
+ * currently recovers these declarations through an ERROR subtree, while the
5
+ * existing Dart ingestion path already handles `extension Name on Type`.
6
+ */
7
+ export declare function preprocessDartExtensionTypes(sourceText: string): string;
@@ -0,0 +1,30 @@
1
+ const DART_EXTENSION_TYPE_HEADER = /\b(extension)([ \t]+type[ \t]+(?:const[ \t]+)?)([A-Za-z_$][A-Za-z0-9_$]*)([ \t]*<[^()\r\n]*>)?([ \t]*)\(([^()\r\n]*)\)/g;
2
+ function representationType(representation) {
3
+ const trimmed = representation.trim();
4
+ const match = /^(.*\S)\s+[A-Za-z_$][A-Za-z0-9_$]*$/.exec(trimmed);
5
+ const typeName = match?.[1]?.trim();
6
+ return typeName && typeName.length > 0 ? typeName : null;
7
+ }
8
+ /**
9
+ * Rewrites Dart 3.3 `extension type Name(Type value)` headers into ordinary
10
+ * extension headers before tree-sitter sees the source. The vendored grammar
11
+ * currently recovers these declarations through an ERROR subtree, while the
12
+ * existing Dart ingestion path already handles `extension Name on Type`.
13
+ */
14
+ export function preprocessDartExtensionTypes(sourceText) {
15
+ return sourceText.replace(DART_EXTENSION_TYPE_HEADER, (match, extensionKeyword, extensionTypeGap, name, typeParameters, beforeRepresentation, representation) => {
16
+ const typeName = representationType(representation);
17
+ if (typeName === null)
18
+ return match;
19
+ const representationSpanLength = beforeRepresentation.length + representation.length + 2;
20
+ const rewrittenRepresentation = ` on ${typeName}`;
21
+ if (rewrittenRepresentation.length > representationSpanLength)
22
+ return match;
23
+ return (extensionKeyword +
24
+ ' '.repeat(extensionTypeGap.length) +
25
+ name +
26
+ (typeParameters ?? '') +
27
+ rewrittenRepresentation +
28
+ ' '.repeat(representationSpanLength - rewrittenRepresentation.length));
29
+ });
30
+ }
@@ -29,6 +29,7 @@ import { createCallExtractor } from '../call-extractors/generic.js';
29
29
  import { dartCallConfig } from '../call-extractors/configs/dart.js';
30
30
  import { emitDartScopeCaptures, interpretDartImport, interpretDartTypeBinding, dartBindingScopeFor, dartImportOwningScope, dartReceiverBinding, dartMergeBindings, dartArityCompatibility, } from './dart/index.js';
31
31
  import { DART_BUILT_INS } from './dart/built-ins.js';
32
+ import { preprocessDartExtensionTypes } from './dart/extension-type-preprocess.js';
32
33
  /**
33
34
  * Resolve the enclosing function from a `function_body` node by looking at its
34
35
  * previous sibling. In Dart's tree-sitter grammar, function_signature and
@@ -100,6 +101,7 @@ export const dartProvider = defineLanguage({
100
101
  },
101
102
  ],
102
103
  treeSitterQueries: DART_QUERIES,
104
+ preprocessSource: preprocessDartExtensionTypes,
103
105
  typeConfig: dartConfig,
104
106
  exportChecker: dartExportChecker,
105
107
  importResolver: createImportResolver(dartImportConfig),
@@ -209,7 +209,7 @@ export const CONTAINER_TYPE_TO_LABEL = {
209
209
  record_declaration: 'Record',
210
210
  protocol_declaration: 'Interface',
211
211
  mixin_declaration: 'Mixin',
212
- extension_declaration: 'Extension',
212
+ extension_declaration: 'Class',
213
213
  class: 'Class',
214
214
  // Ruby `module` declarations map to `Trait` so they participate in the
215
215
  // class-like type registry used by `lookupClassByName` / inheritance
@@ -39,8 +39,15 @@ export declare function resolveNativeSafeStorageDir(storagePath: string, subdir:
39
39
  * `true` (0.16.0). Existing call sites that relied on the positional
40
40
  * default must now pass `false` explicitly to preserve behaviour.
41
41
  *
42
- * Putting both in one shared module guarantees every `new lbug.Database(...)`
43
- * call site agrees on the same ceiling and behaviour.
42
+ * 3. `bufferManagerSize` (not a 0.16.0 change, same pin-explicitly
43
+ * principle): `0` means "native default", and the native default buffer
44
+ * pool is 80% of physical RAM. A long-lived `gitnexus mcp` process or a
45
+ * large incremental analyze can balloon to that ceiling and OOM-kill the
46
+ * host session (#2557), so GitNexus pins an explicit bounded pool — see
47
+ * `resolveBufferManagerSize`.
48
+ *
49
+ * Putting these in one shared module guarantees every `new lbug.Database(...)`
50
+ * call site agrees on the same ceilings and behaviour.
44
51
  */
45
52
  /**
46
53
  * Upper bound for any single GitNexus LadybugDB file (graph index, group
@@ -227,8 +227,15 @@ export function resolveNativeSafeStorageDir(storagePath, subdir) {
227
227
  * `true` (0.16.0). Existing call sites that relied on the positional
228
228
  * default must now pass `false` explicitly to preserve behaviour.
229
229
  *
230
- * Putting both in one shared module guarantees every `new lbug.Database(...)`
231
- * call site agrees on the same ceiling and behaviour.
230
+ * 3. `bufferManagerSize` (not a 0.16.0 change, same pin-explicitly
231
+ * principle): `0` means "native default", and the native default buffer
232
+ * pool is 80% of physical RAM. A long-lived `gitnexus mcp` process or a
233
+ * large incremental analyze can balloon to that ceiling and OOM-kill the
234
+ * host session (#2557), so GitNexus pins an explicit bounded pool — see
235
+ * `resolveBufferManagerSize`.
236
+ *
237
+ * Putting these in one shared module guarantees every `new lbug.Database(...)`
238
+ * call site agrees on the same ceilings and behaviour.
232
239
  */
233
240
  /**
234
241
  * Upper bound for any single GitNexus LadybugDB file (graph index, group
@@ -286,6 +293,53 @@ const resolveCheckpointThreshold = () => {
286
293
  }
287
294
  return DEFAULT_WAL_CHECKPOINT_THRESHOLD;
288
295
  };
296
+ /**
297
+ * Default ceiling for the LadybugDB buffer pool in bytes (#2557).
298
+ *
299
+ * The pool is a page cache with eviction, so the ceiling trades throughput
300
+ * on very large working sets for a machine that stays alive: 2 GiB is
301
+ * ~40× the GitNexus self-index, while the native 80%-of-RAM default let a
302
+ * `detect_changes` call grow a 105 MiB on-disk index to 19.5 GiB RSS and
303
+ * OOM-kill the reporter's session. The `min` with 80% of `os.totalmem()`
304
+ * keeps sub-2.5-GiB machines at the native-equivalent bound (no regression
305
+ * there); the 64 MiB floor keeps tiny containers above any plausible
306
+ * native minimum pool size.
307
+ */
308
+ const DEFAULT_BUFFER_POOL_CAP = 2 * 1024 * 1024 * 1024;
309
+ const BUFFER_POOL_FLOOR = 64 * 1024 * 1024;
310
+ const parseBufferPoolSize = (raw) => {
311
+ if (raw === undefined)
312
+ return undefined;
313
+ const normalized = raw.trim();
314
+ if (normalized.length === 0)
315
+ return undefined;
316
+ const parsed = Number(normalized);
317
+ if (!Number.isFinite(parsed) || parsed < 0)
318
+ return undefined;
319
+ return Math.floor(parsed);
320
+ };
321
+ const defaultBufferPoolSize = () => Math.min(DEFAULT_BUFFER_POOL_CAP, Math.max(BUFFER_POOL_FLOOR, Math.floor(os.totalmem() * 0.8)));
322
+ /**
323
+ * Resolve the `bufferManagerSize` passed to every `new lbug.Database(...)`.
324
+ * `GITNEXUS_LBUG_BUFFER_POOL_SIZE` (bytes) overrides the default; `0` is a
325
+ * deliberate escape hatch that restores LadybugDB's native unbounded
326
+ * 80%-of-RAM default. Resolved at call time (not module load) so tests can
327
+ * stub the env var and `os.totalmem`.
328
+ */
329
+ const resolveBufferManagerSize = () => {
330
+ const raw = process.env.GITNEXUS_LBUG_BUFFER_POOL_SIZE;
331
+ if (raw === undefined)
332
+ return defaultBufferPoolSize();
333
+ const parsed = parseBufferPoolSize(raw);
334
+ if (parsed !== undefined)
335
+ return parsed;
336
+ // Non-empty but unparseable input: warn the operator and fall back —
337
+ // mirrors the GITNEXUS_WAL_CHECKPOINT_THRESHOLD env path above.
338
+ if (raw.trim().length > 0) {
339
+ logger.warn({ rawValue: raw, fallback: defaultBufferPoolSize() }, `Ignoring invalid GITNEXUS_LBUG_BUFFER_POOL_SIZE=${raw}; expected integer >= 0 (bytes; 0 restores the native 80%-of-RAM default); falling back to min(2 GiB, 80% of RAM).`);
340
+ }
341
+ return defaultBufferPoolSize();
342
+ };
289
343
  /** Matches WAL corruption errors from the LadybugDB engine. */
290
344
  const WAL_CORRUPTION_RE = /corrupt(ed)?\s+wal|invalid\s+wal\s+record|wal.*corrupt|checksum.*wal/i;
291
345
  export const WAL_RECOVERY_SUGGESTION = 'WAL corruption detected. Run `gitnexus analyze --force` to rebuild the index.';
@@ -496,7 +550,7 @@ export const classifyDeleteAllError = (err) => {
496
550
  };
497
551
  export function createLbugDatabase(lbugModule, databasePath, options = {}) {
498
552
  // .d.ts declares fewer args than the native constructor accepts.
499
- return new lbugModule.Database(databasePath, 0, // bufferManagerSize
553
+ return new lbugModule.Database(databasePath, resolveBufferManagerSize(), // bufferManagerSize (#2557: default min(2 GiB, 80% RAM); GITNEXUS_LBUG_BUFFER_POOL_SIZE overrides; 0 restores the native 80%-of-RAM default)
500
554
  false, // enableCompression (pinned for v0.16.0)
501
555
  options.readOnly ?? false, LBUG_MAX_DB_SIZE, true, // autoCheckpoint (always on)
502
556
  resolveCheckpointThreshold(), // checkpointThreshold (default 64 MiB; override with GITNEXUS_WAL_CHECKPOINT_THRESHOLD; -1 keeps Ladybug stock ~16 MiB)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.47",
3
+ "version": "1.6.10-rc.49",
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",