gitnexus 1.6.9 → 1.6.10-rc.10

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 (91) hide show
  1. package/README.md +126 -41
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.js +106 -3
  5. package/dist/cli/cli-message.d.ts +1 -1
  6. package/dist/cli/doctor.d.ts +5 -0
  7. package/dist/cli/doctor.js +41 -3
  8. package/dist/cli/editor-targets.d.ts +17 -2
  9. package/dist/cli/editor-targets.js +44 -0
  10. package/dist/cli/embeddings.d.ts +12 -0
  11. package/dist/cli/embeddings.js +49 -0
  12. package/dist/cli/help-i18n.js +4 -0
  13. package/dist/cli/i18n/en.d.ts +5 -1
  14. package/dist/cli/i18n/en.js +5 -1
  15. package/dist/cli/i18n/resources.d.ts +9 -1
  16. package/dist/cli/i18n/zh-CN.d.ts +4 -0
  17. package/dist/cli/i18n/zh-CN.js +5 -1
  18. package/dist/cli/index.js +12 -1
  19. package/dist/cli/setup.js +204 -28
  20. package/dist/cli/uninstall.js +70 -16
  21. package/dist/core/embeddings/embedder.js +16 -2
  22. package/dist/core/embeddings/http-client.d.ts +28 -1
  23. package/dist/core/embeddings/http-client.js +107 -15
  24. package/dist/core/embeddings/node-module-compat.d.ts +23 -0
  25. package/dist/core/embeddings/node-module-compat.js +23 -0
  26. package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
  27. package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
  28. package/dist/core/embeddings/runtime-install.d.ts +119 -0
  29. package/dist/core/embeddings/runtime-install.js +372 -0
  30. package/dist/core/embeddings/runtime-support.d.ts +48 -17
  31. package/dist/core/embeddings/runtime-support.js +96 -0
  32. package/dist/core/group/extractors/http-patterns/python.js +246 -108
  33. package/dist/core/group/extractors/manifest-extractor.js +4 -0
  34. package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
  35. package/dist/core/ingestion/cobol-processor.js +27 -26
  36. package/dist/core/ingestion/community-processor.d.ts +51 -1
  37. package/dist/core/ingestion/community-processor.js +316 -63
  38. package/dist/core/ingestion/emit-references.js +3 -2
  39. package/dist/core/ingestion/markdown-processor.js +3 -2
  40. package/dist/core/ingestion/parsing-processor.d.ts +3 -1
  41. package/dist/core/ingestion/parsing-processor.js +4 -0
  42. package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
  43. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
  44. package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
  45. package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
  46. package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
  47. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  48. package/dist/core/ingestion/tree-sitter-queries.js +12 -1
  49. package/dist/core/ingestion/utils/line-base.d.ts +20 -0
  50. package/dist/core/ingestion/utils/line-base.js +20 -0
  51. package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
  52. package/dist/core/ingestion/utils/symbol-labels.js +45 -0
  53. package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
  54. package/dist/core/ingestion/workers/parse-worker.js +39 -4
  55. package/dist/core/ingestion/workers/result-merge.js +4 -0
  56. package/dist/core/lbug/csv-generator.js +8 -2
  57. package/dist/core/lbug/extension-load-error.d.ts +67 -0
  58. package/dist/core/lbug/extension-load-error.js +320 -0
  59. package/dist/core/lbug/extension-loader.d.ts +17 -2
  60. package/dist/core/lbug/extension-loader.js +38 -13
  61. package/dist/core/lbug/lbug-adapter.js +7 -16
  62. package/dist/core/lbug/native-check.d.ts +22 -0
  63. package/dist/core/lbug/native-check.js +66 -0
  64. package/dist/core/lbug/pool-adapter.js +10 -1
  65. package/dist/core/lbug/sidecar-recovery.d.ts +32 -0
  66. package/dist/core/lbug/sidecar-recovery.js +91 -7
  67. package/dist/core/platform/capabilities.js +27 -1
  68. package/dist/core/run-analyze.js +33 -6
  69. package/dist/core/search/fts-indexes.d.ts +7 -0
  70. package/dist/core/search/fts-indexes.js +37 -0
  71. package/dist/mcp/core/embedder.js +16 -2
  72. package/dist/mcp/local/line-display.d.ts +22 -0
  73. package/dist/mcp/local/line-display.js +3 -0
  74. package/dist/mcp/local/local-backend.d.ts +14 -0
  75. package/dist/mcp/local/local-backend.js +63 -19
  76. package/dist/mcp/local/pdg-impact.d.ts +5 -3
  77. package/dist/mcp/local/pdg-impact.js +5 -2
  78. package/dist/mcp/resources.js +1 -0
  79. package/dist/mcp/tools.js +1 -1
  80. package/dist/server/api.js +2 -2
  81. package/dist/storage/parse-cache.js +1 -1
  82. package/dist/storage/repo-manager.d.ts +6 -1
  83. package/dist/storage/repo-manager.js +6 -1
  84. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
  85. package/hooks/claude/gitnexus-hook.cjs +59 -9
  86. package/package.json +5 -3
  87. package/scripts/cross-platform-tests.ts +26 -0
  88. package/scripts/ensure-fts.ts +32 -0
  89. package/scripts/install-duckdb-extension.mjs +85 -24
  90. package/scripts/run-cross-platform.ts +46 -6
  91. package/scripts/shard-arg.ts +30 -0
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Warning attached to search responses when BM25/FTS is degraded. Prefers the
3
+ * live extension-load failure (with LadybugDB's real reason, #2374) over the
4
+ * generic indexes-missing message, so "indexes exist but the extension broke"
5
+ * is not misreported as missing indexes.
6
+ */
7
+ export declare const ftsDegradedWarning: () => string;
1
8
  export declare const SUPPORTED_FTS_STEMMERS: ReadonlySet<string>;
2
9
  export interface CreateSearchFTSIndexesOptions {
3
10
  onIndexStart?: (table: string, indexName: string) => void;
@@ -1,5 +1,42 @@
1
1
  import { createFTSIndex, dropFTSIndex, DEFAULT_FTS_STEMMER } from '../lbug/lbug-adapter.js';
2
+ import { getExtensionCapabilities } from '../lbug/extension-loader.js';
3
+ import { classifyExtensionLoadError } from '../lbug/extension-load-error.js';
2
4
  import { FTS_INDEXES } from './fts-schema.js';
5
+ /**
6
+ * Strip filesystem paths from a LadybugDB error before it reaches the HTTP
7
+ * `/api/search` and MCP query surfaces (#2374, PR #2375): the raw LOAD error
8
+ * embeds the absolute extension path (username, home dir) which must not leak to
9
+ * a network client. The error class words ("Failed to load library", "invalid
10
+ * ELF header", "has not been installed") have no leading path separator and
11
+ * survive. CLI/doctor/log surfaces keep the full path (they read the reason
12
+ * directly, not through this function).
13
+ */
14
+ const redactPaths = (reason) => reason.replace(/(?:[A-Za-z]:\\|\/)[^\s'"]+/g, '<path>');
15
+ /**
16
+ * Warning attached to search responses when BM25/FTS is degraded. Prefers the
17
+ * live extension-load failure (with LadybugDB's real reason, #2374) over the
18
+ * generic indexes-missing message, so "indexes exist but the extension broke"
19
+ * is not misreported as missing indexes.
20
+ */
21
+ export const ftsDegradedWarning = () => {
22
+ const fts = getExtensionCapabilities().find((c) => c.name === 'fts');
23
+ if (fts && !fts.loaded) {
24
+ const reason = fts.reason ? redactPaths(fts.reason).replace(/\.$/, '') : undefined;
25
+ // A missing *runtime dependency* (Windows error 126, etc.) is not healed by
26
+ // reinstalling (#2374) — surface the classified remedy instead of the generic
27
+ // reinstall tail. Read the diagnosis cached at mark-unavailable time so this
28
+ // per-request path (HTTP /api/search + MCP query) does NO file I/O (#2383 F3);
29
+ // fall back to the pure, no-I/O string classifier if it is somehow absent.
30
+ const { kind, remedy } = fts.diagnosis ?? classifyExtensionLoadError(fts.reason);
31
+ const tail = kind === 'missing_dependency'
32
+ ? ` ${remedy}`
33
+ : '. Run `gitnexus doctor` for details, then `gitnexus analyze --repair-fts` with network access to reinstall.';
34
+ return ('FTS extension failed to load — keyword search degraded' +
35
+ (reason ? ` (${reason})` : '') +
36
+ tail);
37
+ }
38
+ return 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.';
39
+ };
3
40
  // Stemmers shipped by the LadybugDB FTS extension. Mirrors the lowercase token
4
41
  // set in the extension bundled with @ladybugdb/core 0.18.x (see package.json).
5
42
  // Keep in sync on a LadybugDB minor bump — a value here that the installed
@@ -7,8 +7,9 @@
7
7
  import { isHttpMode, getHttpDimensions, httpEmbedQuery, } from '../../core/embeddings/http-client.js';
8
8
  import { resolveEmbeddingConfig } from '../../core/embeddings/config.js';
9
9
  import { applyHfEnvOverrides, isHfDownloadFailure, withHfDownloadRetry, } from '../../core/embeddings/hf-env.js';
10
- import { getLocalEmbeddingRuntimeBlocker } from '../../core/embeddings/runtime-support.js';
10
+ import { getLocalEmbeddingRuntimeBlocker, getMissingLocalEmbeddingStackMessage, } from '../../core/embeddings/runtime-support.js';
11
11
  import { ensureOnnxRuntimeCommonResolvable } from '../../core/embeddings/onnxruntime-common-resolver.js';
12
+ import { ensureEmbeddingStackResolvable } from '../../core/embeddings/runtime-install.js';
12
13
  import { ensureOnnxRuntimeNodeMatchesSystem } from '../../core/embeddings/onnxruntime-node-resolver.js';
13
14
  import { silenceStdout, restoreStdout, realStderrWrite } from '../../core/lbug/pool-adapter.js';
14
15
  import { logger } from '../../core/logger.js';
@@ -45,6 +46,11 @@ export const initEmbedder = async () => {
45
46
  try {
46
47
  // Lazy-load transformers.js only after the runtime guard has passed, so
47
48
  // unsupported platforms never reach the native ONNX import (#1515).
49
+ // Registered FIRST so it sits last in the hook chain (registerHooks runs
50
+ // the most recent hook first): when the optional stack was pruned at
51
+ // install time (#2370), its bare specifiers fall back to the on-demand
52
+ // runtime prefix.
53
+ ensureEmbeddingStackResolvable();
48
54
  // Under pnpm-strict / `pnpm dlx`, transformers' phantom `onnxruntime-common`
49
55
  // import is unresolvable; register the fallback resolver first (#307).
50
56
  ensureOnnxRuntimeCommonResolvable();
@@ -55,7 +61,15 @@ export const initEmbedder = async () => {
55
61
  // Windows/DirectML, and macOS. Mirrors the core embedder's call site so
56
62
  // MCP query-time embedding gets the same CUDA-13 fix.
57
63
  ensureOnnxRuntimeNodeMatchesSystem();
58
- const { pipeline, env } = await import('@huggingface/transformers');
64
+ // The stack is an optionalDependency: npm prunes it when onnxruntime-node's
65
+ // postinstall can't reach api.nuget.org (#2370). Rethrow with actionable
66
+ // reinstall guidance instead of a raw ERR_MODULE_NOT_FOUND.
67
+ const { pipeline, env } = await import('@huggingface/transformers').catch((err) => {
68
+ const missing = getMissingLocalEmbeddingStackMessage(err);
69
+ if (missing)
70
+ throw new Error(missing);
71
+ throw err;
72
+ });
59
73
  env.allowLocalModels = false;
60
74
  // Bridge user-controlled env vars to transformers.js: HF_HOME →
61
75
  // env.cacheDir, HF_ENDPOINT → env.remoteHost (#1205). Centralised in
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Convert a 0-based GraphNode `startLine`/`endLine` to the 1-based line number
3
+ * shown to humans and LLMs in MCP tool output.
4
+ *
5
+ * Storage is 0-based (tree-sitter `startPosition.row`; see
6
+ * `ingestion/utils/line-base.ts`), which matches editors/`sed`/`less -N` only
7
+ * after `+ 1`. The `context`, `query`, and `impact` tools present line numbers a
8
+ * user cross-references against source, so they convert here at the response
9
+ * boundary (#2377).
10
+ *
11
+ * Apply ONLY to a symbol node's 0-based `startLine`/`endLine`. Do NOT apply to:
12
+ * - BasicBlock / CFG `functionStartLine` and PDG statement lines — already
13
+ * 1-based (they use `startPosition.row + 1`);
14
+ * - the internal `sym.startLine + 1` join params that target the 1-based
15
+ * BasicBlock id space;
16
+ * - raw `cypher` results, which pass LadybugDB columns through verbatim and
17
+ * stay 0-based (documented).
18
+ *
19
+ * `undefined`/`null` pass through so optional line fields stay absent.
20
+ */
21
+ export declare function toDisplayLine(zeroBasedLine: number): number;
22
+ export declare function toDisplayLine(zeroBasedLine: number | null | undefined): number | undefined;
@@ -0,0 +1,3 @@
1
+ export function toDisplayLine(zeroBasedLine) {
2
+ return typeof zeroBasedLine === 'number' ? zeroBasedLine + 1 : undefined;
3
+ }
@@ -162,10 +162,24 @@ export declare class LocalBackend {
162
162
  * stderr noisy per DoD §2.8.
163
163
  */
164
164
  private warnedVectorUnsupported;
165
+ /**
166
+ * One-shot warning when a pruned or Node-unloadable optional embedding stack
167
+ * (#2370/#2372) forces semantic search to fall back to BM25 — so the
168
+ * degradation is visible once instead of silent.
169
+ */
170
+ private warnedMissingEmbeddingStack;
165
171
  /**
166
172
  * Cross-repo group tools (CLI). Shares logic with MCP `group_*` handlers.
167
173
  */
168
174
  getGroupService(): GroupService;
175
+ /**
176
+ * Adapt local `trace` to the group port. The assembled group/cross-repo trace
177
+ * presents 1-based endpoints (via resolveSymbolForGroup), so convert the hop
178
+ * lines here too — otherwise one response mixes 1-based endpoints with 0-based
179
+ * hops (#2380). Single-repo `trace` dispatches directly (not through this
180
+ * port) and stays 0-based (documented full-parity follow-up).
181
+ */
182
+ private traceForGroup;
169
183
  /**
170
184
  * Adapt the shared symbol resolver to the GroupToolPort contract. Used by the
171
185
  * cross-repo trace path to locate which member repo an endpoint lives in and
@@ -10,6 +10,7 @@ import path from 'path';
10
10
  import { createHash } from 'crypto';
11
11
  import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, } from '../../core/lbug/pool-adapter.js';
12
12
  import { isValidQueryParams } from '../../core/lbug/query-params.js';
13
+ import { toDisplayLine } from './line-display.js';
13
14
  import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/lbug-config.js';
14
15
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
15
16
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
@@ -26,9 +27,11 @@ import { rankExactEmbeddingRows, } from '../../core/embeddings/exact-search.js';
26
27
  import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME } from '../../core/lbug/schema.js';
27
28
  import { getExactScanLimit, isVectorExtensionSupportedByPlatform, } from '../../core/platform/capabilities.js';
28
29
  import { PhaseTimer } from '../../core/search/phase-timer.js';
30
+ import { ftsDegradedWarning } from '../../core/search/fts-indexes.js';
29
31
  import { cjkSegmentationModeMismatch, containsSegmentableCjkRun, getSearchFTSCjkSegmentation, isSupportedCjkSegmentationMode, MAX_CJK_SEGMENTATION_QUERY_LENGTH, } from '../../core/search/cjk-segmentation.js';
30
32
  import { checkStalenessAsync, checkCwdMatch } from '../../core/git-staleness.js';
31
33
  import { logger } from '../../core/logger.js';
34
+ import { isLocalEmbeddingRuntimeBlockerMessage, isMissingLocalEmbeddingStackMessage, } from '../../core/embeddings/runtime-support.js';
32
35
  import { LIST_REPOS_DEFAULT_LIMIT, LIST_REPOS_MAX_LIMIT, EXPLAIN_DEFAULT_LIMIT, EXPLAIN_MAX_LIMIT, PDG_QUERY_DEFAULT_LIMIT, PDG_QUERY_MAX_LIMIT, } from '../tools.js';
33
36
  import { findImportCycles } from '../../core/graph/import-cycles.js';
34
37
  import { decodeTaintPath } from '../../core/ingestion/taint/path-codec.js';
@@ -434,6 +437,12 @@ export class LocalBackend {
434
437
  * stderr noisy per DoD §2.8.
435
438
  */
436
439
  warnedVectorUnsupported = false;
440
+ /**
441
+ * One-shot warning when a pruned or Node-unloadable optional embedding stack
442
+ * (#2370/#2372) forces semantic search to fall back to BM25 — so the
443
+ * degradation is visible once instead of silent.
444
+ */
445
+ warnedMissingEmbeddingStack = false;
437
446
  /**
438
447
  * Cross-repo group tools (CLI). Shares logic with MCP `group_*` handlers.
439
448
  */
@@ -445,7 +454,7 @@ export class LocalBackend {
445
454
  query: (r, p) => this.query(r, p),
446
455
  impactByUid: (id, uid, d, o) => this.impactByUid(id, uid, d, o),
447
456
  context: (r, p) => this.context(r, p),
448
- trace: (r, p) => this.trace(r, p),
457
+ trace: (r, p) => this.traceForGroup(r, p),
449
458
  resolveSymbol: (r, q) => this.resolveSymbolForGroup(r, q),
450
459
  pdgFlows: (r, anchor, opts) => this.pdgFlowsForGroup(r, anchor, opts),
451
460
  };
@@ -453,6 +462,23 @@ export class LocalBackend {
453
462
  }
454
463
  return this.groupToolSvc;
455
464
  }
465
+ /**
466
+ * Adapt local `trace` to the group port. The assembled group/cross-repo trace
467
+ * presents 1-based endpoints (via resolveSymbolForGroup), so convert the hop
468
+ * lines here too — otherwise one response mixes 1-based endpoints with 0-based
469
+ * hops (#2380). Single-repo `trace` dispatches directly (not through this
470
+ * port) and stays 0-based (documented full-parity follow-up).
471
+ */
472
+ async traceForGroup(repo, params) {
473
+ const result = await this.trace(repo, params);
474
+ const hops = result.hops;
475
+ if (Array.isArray(hops)) {
476
+ for (const hop of hops) {
477
+ hop.startLine = toDisplayLine(hop.startLine);
478
+ }
479
+ }
480
+ return result;
481
+ }
456
482
  /**
457
483
  * Adapt the shared symbol resolver to the GroupToolPort contract. Used by the
458
484
  * cross-repo trace path to locate which member repo an endpoint lives in and
@@ -470,8 +496,8 @@ export class LocalBackend {
470
496
  name: s.name,
471
497
  type: s.type,
472
498
  filePath: s.filePath,
473
- startLine: s.startLine,
474
- endLine: s.endLine,
499
+ startLine: toDisplayLine(s.startLine),
500
+ endLine: toDisplayLine(s.endLine),
475
501
  },
476
502
  };
477
503
  }
@@ -483,7 +509,7 @@ export class LocalBackend {
483
509
  name: c.name,
484
510
  type: c.type,
485
511
  filePath: c.filePath,
486
- startLine: c.startLine,
512
+ startLine: toDisplayLine(c.startLine),
487
513
  })),
488
514
  };
489
515
  }
@@ -1564,8 +1590,8 @@ export class LocalBackend {
1564
1590
  name: sym.name,
1565
1591
  type: sym.type,
1566
1592
  filePath: sym.filePath,
1567
- startLine: sym.startLine,
1568
- endLine: sym.endLine,
1593
+ startLine: toDisplayLine(sym.startLine),
1594
+ endLine: toDisplayLine(sym.endLine),
1569
1595
  ...(module ? { module } : {}),
1570
1596
  ...(includeContent && content ? { content } : {}),
1571
1597
  };
@@ -1652,7 +1678,7 @@ export class LocalBackend {
1652
1678
  // path, leaving the success-path response shape byte-identical.
1653
1679
  const warnings = [];
1654
1680
  if (!ftsUsed) {
1655
- warnings.push('FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.');
1681
+ warnings.push(ftsDegradedWarning());
1656
1682
  }
1657
1683
  // #2331: a CJK query against a server process resolving
1658
1684
  // GITNEXUS_FTS_CJK_SEGMENTATION to 'none' silently misses sub-phrase
@@ -1791,8 +1817,11 @@ export class LocalBackend {
1791
1817
  name: sym.name || sym[1],
1792
1818
  type: sym.type || sym[2],
1793
1819
  filePath: sym.filePath || sym[3],
1794
- startLine: sym.startLine || sym[4],
1795
- endLine: sym.endLine || sym[5],
1820
+ // Raw 0-based here — `bm25Search` is only called from `query()`,
1821
+ // whose aggregation loop applies `toDisplayLine` once (see below).
1822
+ // Converting here too would double-shift BM25-matched lines (#2380).
1823
+ startLine: sym.startLine ?? sym[4],
1824
+ endLine: sym.endLine ?? sym[5],
1796
1825
  bm25Score: bm25Result.score,
1797
1826
  });
1798
1827
  }
@@ -1927,8 +1956,20 @@ export class LocalBackend {
1927
1956
  }
1928
1957
  return results;
1929
1958
  }
1930
- catch {
1931
- // Expected when embeddings are disabled silently fall back to BM25-only
1959
+ catch (err) {
1960
+ // Embeddings disabled is the common, silent case. But a pruned or
1961
+ // Node-unloadable optional stack (#2370/#2372) also lands here — surface it
1962
+ // once so semantic search doesn't silently degrade to BM25 with no hint
1963
+ // (the exact silent-degradation mode #2370 exists to fix). Emitted once per
1964
+ // LocalBackend instance to keep stderr quiet on hot paths (like the VECTOR
1965
+ // fallback above). All other errors stay silent, as before.
1966
+ const message = err instanceof Error ? err.message : '';
1967
+ if (!this.warnedMissingEmbeddingStack &&
1968
+ (isMissingLocalEmbeddingStackMessage(message) ||
1969
+ isLocalEmbeddingRuntimeBlockerMessage(message))) {
1970
+ this.warnedMissingEmbeddingStack = true;
1971
+ logger.warn(`GitNexus [query:vector]: ${message}`);
1972
+ }
1932
1973
  return [];
1933
1974
  }
1934
1975
  }
@@ -2376,7 +2417,7 @@ export class LocalBackend {
2376
2417
  name: c.name,
2377
2418
  kind: c.type,
2378
2419
  filePath: c.filePath,
2379
- line: c.startLine,
2420
+ line: toDisplayLine(c.startLine),
2380
2421
  score: Number(c.score.toFixed(2)),
2381
2422
  })),
2382
2423
  };
@@ -2583,8 +2624,8 @@ export class LocalBackend {
2583
2624
  name: sym.name || sym[1],
2584
2625
  kind: symKind,
2585
2626
  filePath: sym.filePath || sym[3],
2586
- startLine: sym.startLine || sym[4],
2587
- endLine: sym.endLine || sym[5],
2627
+ startLine: toDisplayLine(sym.startLine ?? sym[4]),
2628
+ endLine: toDisplayLine(sym.endLine ?? sym[5]),
2588
2629
  ...(include_content && (sym.content || sym[6]) ? { content: sym.content || sym[6] } : {}),
2589
2630
  ...(methodMetadata ? { methodMetadata } : {}),
2590
2631
  },
@@ -2660,7 +2701,7 @@ export class LocalBackend {
2660
2701
  name: c.name,
2661
2702
  kind: c.type,
2662
2703
  filePath: c.filePath,
2663
- line: c.startLine,
2704
+ line: toDisplayLine(c.startLine),
2664
2705
  score: Number(c.score.toFixed(2)),
2665
2706
  })),
2666
2707
  },
@@ -2678,11 +2719,14 @@ export class LocalBackend {
2678
2719
  return {
2679
2720
  anchorClause: 'a.id STARTS WITH $idPrefix AND a.startLine >= $symStart AND a.startLine <= $symEnd',
2680
2721
  queryParams: { idPrefix, symStart: sym.startLine + 1, symEnd: sym.endLine + 1 },
2722
+ // Display anchor is 1-based, matching the ambiguous-candidate branch and
2723
+ // the context/query/impact tools (#2380). This is display-only — the
2724
+ // BasicBlock join above uses the raw `sym.startLine + 1` in `symStart`.
2681
2725
  anchor: {
2682
2726
  file: sym.filePath,
2683
2727
  symbol: sym.name,
2684
- startLine: sym.startLine,
2685
- endLine: sym.endLine,
2728
+ startLine: toDisplayLine(sym.startLine),
2729
+ endLine: toDisplayLine(sym.endLine),
2686
2730
  },
2687
2731
  };
2688
2732
  }
@@ -4016,7 +4060,7 @@ export class LocalBackend {
4016
4060
  name: c.name,
4017
4061
  kind: c.type,
4018
4062
  filePath: c.filePath,
4019
- line: c.startLine,
4063
+ line: toDisplayLine(c.startLine),
4020
4064
  score: Number(c.score.toFixed(2)),
4021
4065
  })),
4022
4066
  };
@@ -4069,7 +4113,7 @@ export class LocalBackend {
4069
4113
  name: c.name,
4070
4114
  kind: c.type,
4071
4115
  filePath: c.filePath,
4072
- line: c.startLine,
4116
+ line: toDisplayLine(c.startLine),
4073
4117
  score: Number(c.score.toFixed(2)),
4074
4118
  impactedCount: summary?.impactedCount ?? 0,
4075
4119
  risk: summary?.risk ?? 'UNKNOWN',
@@ -34,8 +34,10 @@ export declare function splitCalleeIds(raw: unknown): string[];
34
34
  * Contract version of the mode:'pdg' impact result shape. A stable discriminator
35
35
  * for external MCP/agent consumers — distinct from the DB INCREMENTAL_SCHEMA_VERSION.
36
36
  * Bump on any breaking change to the PDG result fields.
37
+ * v2: `startLine` in the result is now 1-based display (#2380), matching the
38
+ * context/query/impact tools (was 0-based).
37
39
  */
38
- export declare const PDG_RESULT_VERSION: 1;
40
+ export declare const PDG_RESULT_VERSION: 2;
39
41
  /** A reachable dependence block resolved to its source statement. */
40
42
  export interface PdgStatement {
41
43
  /** 1-based source line where the statement's block starts. */
@@ -105,7 +107,7 @@ export interface PdgInterproceduralImpact {
105
107
  export interface PdgImpactBaseResult extends PdgImpactParityFields {
106
108
  mode: 'pdg';
107
109
  /** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
108
- pdgResultVersion: 1;
110
+ pdgResultVersion: 2;
109
111
  target: PdgImpactTarget;
110
112
  direction: 'upstream' | 'downstream';
111
113
  impactedCount: number;
@@ -172,7 +174,7 @@ export interface PdgImpactDegradedResult extends PdgImpactBaseResult {
172
174
  export interface PdgImpactErrorResult {
173
175
  mode?: 'pdg';
174
176
  /** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
175
- pdgResultVersion: 1;
177
+ pdgResultVersion: 2;
176
178
  error: string;
177
179
  target: PdgImpactTarget;
178
180
  direction: 'upstream' | 'downstream';
@@ -9,6 +9,7 @@ import path from 'path';
9
9
  import { loadMeta } from '../../storage/repo-manager.js';
10
10
  import { IMPACT_MAX_DEPTH, PDG_QUERY_DEFAULT_LIMIT, PDG_QUERY_MAX_LIMIT } from '../tools.js';
11
11
  import { CALLEES_TRUNCATED_SENTINEL, CALLEE_ID_SEP } from '../../core/ingestion/cfg/emit.js';
12
+ import { toDisplayLine } from './line-display.js';
12
13
  import { decodeCallSummary } from '../../core/ingestion/taint/call-summary-codec.js';
13
14
  import { decodeReachingDefReason } from '../../core/ingestion/cfg/reaching-def-reason-codec.js';
14
15
  import { getProviderForFile } from '../../core/ingestion/languages/index.js';
@@ -84,8 +85,10 @@ export function splitCalleeIds(raw) {
84
85
  * Contract version of the mode:'pdg' impact result shape. A stable discriminator
85
86
  * for external MCP/agent consumers — distinct from the DB INCREMENTAL_SCHEMA_VERSION.
86
87
  * Bump on any breaking change to the PDG result fields.
88
+ * v2: `startLine` in the result is now 1-based display (#2380), matching the
89
+ * context/query/impact tools (was 0-based).
87
90
  */
88
- export const PDG_RESULT_VERSION = 1;
91
+ export const PDG_RESULT_VERSION = 2;
89
92
  /**
90
93
  * FU-B-2 intra-block def→use line walk. Given a block's self REACHING_DEF
91
94
  * def→use line PAIRS (every `defLine → useLine` step decoded from the edge
@@ -519,7 +522,7 @@ function assemblePdgImpactResult(input) {
519
522
  name: s.name,
520
523
  type: s.type,
521
524
  filePath: s.filePath,
522
- ...(s.startLine !== undefined ? { startLine: s.startLine } : {}),
525
+ ...(s.startLine !== undefined ? { startLine: toDisplayLine(s.startLine) } : {}),
523
526
  ...(s.ambiguous ? { ambiguous: true } : {}),
524
527
  ...(s.id === null ? { unresolved: true } : {}),
525
528
  pdgEvidence: (s.id === null ? 'degraded' : 'owner-projection'),
@@ -365,6 +365,7 @@ additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Nam
365
365
 
366
366
  node_properties:
367
367
  common: "name (STRING), filePath (STRING), startLine (INT32), endLine (INT32)"
368
+ line_numbers: "startLine/endLine on symbol nodes are 0-BASED (tree-sitter rows) in storage AND in raw Cypher results. The context, query, impact, group/cross-repo trace, and explain/pdg_query (symbol anchor) tools present them 1-BASED (editor / sed / less -N aligned), so a symbol spans editor lines (startLine+1)..(endLine+1) — e.g. sed '<startLine+1>,<endLine+1>!d' <file>. Single-repo trace symbol lines stay 0-BASED for now (full-parity follow-up). content holds the exact symbol span. (BasicBlock / PDG statement lines are separately 1-based.) (#2377, #2380)"
368
369
  Method: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL), visibility (STRING), isStatic (BOOL), isAbstract (BOOL), isFinal (BOOL), isVirtual (BOOL), isOverride (BOOL), isAsync (BOOL), isPartial (BOOL), requiredParameterCount (INT32), parameterTypes (STRING[]), annotations (STRING[])"
369
370
  Function: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL), visibility (STRING), isStatic (BOOL), isAbstract (BOOL), isFinal (BOOL), isAsync (BOOL), parameterTypes (STRING[]), annotations (STRING[])"
370
371
  Property: "declaredType (STRING) — the field's type annotation (e.g., 'Address', 'City'). Used for field-access chain resolution."
package/dist/mcp/tools.js CHANGED
@@ -379,7 +379,7 @@ MODE (opt-in): "callgraph" (default) walks symbol→symbol edges (CALLS/IMPORTS/
379
379
 
380
380
  STATEMENT-ANCHORED PDG SLICE: with mode:'pdg', pass "line" (1-based source line within the target symbol) to seed the dependence slice on the statement at that line and return what depends on it in affectedStatements (line + text). Inter-procedural symbols are still reported through interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket. Without "line", pdg returns whole-symbol inter-procedural reach plus local whole-symbol PDG diagnostics.
381
381
 
382
- PDG OUTPUT CONTRACT: every mode:'pdg' result (success, empty, degraded, or error) carries pdgResultVersion:1 — a stable discriminator for external consumers that bumps on any breaking change to the PDG result shape (distinct from the DB schema version). Successful PDG results include mode:'pdg', a full target envelope (id/name/type/filePath), affectedStatements, affectedStatementCount, interproceduralByDepth/pdgInterprocedural for cross-function reach, compatibility byDepth/byDepthCounts, risk:'UNKNOWN', and a note describing the unified contract. Degraded PDG results (no-layer, sub-layer-missing, unknown) keep mode:'pdg', pdgResultVersion:1, target metadata when the target resolves, risk:'UNKNOWN', note/remediation, and empty byDepth parity fields — never a false-safe zero. If depth and limit both bound the slice, truncatedByReasons reports both causes while truncatedBy remains scalar.
382
+ PDG OUTPUT CONTRACT: every mode:'pdg' result (success, empty, degraded, or error) carries pdgResultVersion:2 — a stable discriminator for external consumers that bumps on any breaking change to the PDG result shape (distinct from the DB schema version). Successful PDG results include mode:'pdg', a full target envelope (id/name/type/filePath), affectedStatements, affectedStatementCount, interproceduralByDepth/pdgInterprocedural for cross-function reach, compatibility byDepth/byDepthCounts, risk:'UNKNOWN', and a note describing the unified contract. Degraded PDG results (no-layer, sub-layer-missing, unknown) keep mode:'pdg', pdgResultVersion:2, target metadata when the target resolves, risk:'UNKNOWN', note/remediation, and empty byDepth parity fields — never a false-safe zero. If depth and limit both bound the slice, truncatedByReasons reports both causes while truncatedBy remains scalar.
383
383
 
384
384
  WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
385
385
  AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
@@ -18,6 +18,7 @@ import { isValidQueryParams } from '../core/lbug/query-params.js';
18
18
  import { NODE_TABLES } from '../_shared/index.js';
19
19
  import { searchFTSFromLbug } from '../core/search/bm25-index.js';
20
20
  import { hybridSearch } from '../core/search/hybrid-search.js';
21
+ import { ftsDegradedWarning } from '../core/search/fts-indexes.js';
21
22
  import { LocalBackend } from '../mcp/local/local-backend.js';
22
23
  import { mountMCPEndpoints } from './mcp-http.js';
23
24
  import { fileURLToPath } from 'url';
@@ -1140,8 +1141,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1140
1141
  }, { readOnly: true });
1141
1142
  const response = { results: results.searchResults ?? results };
1142
1143
  if (results.ftsAvailable === false) {
1143
- response.warning =
1144
- 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --repair-fts (or gitnexus analyze --force) to rebuild indexes.';
1144
+ response.warning = ftsDegradedWarning();
1145
1145
  }
1146
1146
  res.json(response);
1147
1147
  }
@@ -52,7 +52,7 @@ import { fileURLToPath } from 'url';
52
52
  // the main thread (the #1983 OOM). Because the two stores share this version,
53
53
  // any future change to the `ParsedFile` serialization shape MUST bump
54
54
  // SCHEMA_BUMP so both invalidate in lockstep.
55
- const SCHEMA_BUMP = 10; // PR #2200: Property nodes gained `rawDeclaredType` + `annotations` (Spring DI); warm caches must invalidate or the DI phase silently no-ops on replayed pre-upgrade nodes
55
+ const SCHEMA_BUMP = 12; // #2391 follow-up: extractPythonModuleConstants changed what it EMITS for the same source (binding mutual-exclusivity clears stale imports; RHS refs are snapshotted; `$imp$N` aliases). `moduleConstants` is cached verbatim, so a warm shard built pre-fix would replay stale/WRONG folds and the correctness fixes would silently no-op on upgrade — bump to force re-extraction. (11 = #2391: ExtractedDecoratorRoute gained `routePathExpr`/`routePathOperands` + ParseWorkerResult gained per-file `moduleConstants`. 10 = PR #2200: Property nodes gained `rawDeclaredType` + `annotations` for Spring DI)
56
56
  const GITNEXUS_PKG_VERSION = (() => {
57
57
  try {
58
58
  // package.json sits at gitnexus/package.json — two levels up from
@@ -242,8 +242,13 @@ export interface RepoMeta {
242
242
  * URL-only id). The incremental writeback preserves unchanged-file rows, so a
243
243
  * top-up against a pre-v5 index would strand old url-keyed Route nodes alongside
244
244
  * new composite-keyed ones — force a full re-analyze instead.
245
+ * v6: line-number storage flipped to uniform 0-based for the last 1-based
246
+ * GraphNode emitters — COBOL/JCL/markdown/scope (#2377/#2379/#2380). Incremental
247
+ * writeback preserves unchanged-file rows, so a top-up against a pre-v6 index
248
+ * would MIX old 1-based rows with new 0-based ones — and the 1-based MCP display
249
+ * would render the stale rows one line too high — so force a full re-analyze.
245
250
  */
246
- export declare const INCREMENTAL_SCHEMA_VERSION = 5;
251
+ export declare const INCREMENTAL_SCHEMA_VERSION = 6;
247
252
  export interface IndexedRepo {
248
253
  repoPath: string;
249
254
  storagePath: string;
@@ -90,8 +90,13 @@ export const registryPathEquals = (a, b) => process.platform === 'win32' ? a.toL
90
90
  * URL-only id). The incremental writeback preserves unchanged-file rows, so a
91
91
  * top-up against a pre-v5 index would strand old url-keyed Route nodes alongside
92
92
  * new composite-keyed ones — force a full re-analyze instead.
93
+ * v6: line-number storage flipped to uniform 0-based for the last 1-based
94
+ * GraphNode emitters — COBOL/JCL/markdown/scope (#2377/#2379/#2380). Incremental
95
+ * writeback preserves unchanged-file rows, so a top-up against a pre-v6 index
96
+ * would MIX old 1-based rows with new 0-based ones — and the 1-based MCP display
97
+ * would render the stale rows one line too high — so force a full re-analyze.
93
98
  */
94
- export const INCREMENTAL_SCHEMA_VERSION = 5;
99
+ export const INCREMENTAL_SCHEMA_VERSION = 6;
95
100
  const GITNEXUS_DIR = '.gitnexus';
96
101
  const GITNEXUS_EXCLUDE_ENTRY = `${GITNEXUS_DIR}/`;
97
102
  export const INDEX_METADATA_FILE = 'gitnexus.json';
@@ -391,6 +391,49 @@ function buildAfterToolContext(input) {
391
391
  return parts.length > 0 ? parts.join('\n\n') : null;
392
392
  }
393
393
 
394
+ /**
395
+ * Fallback augmentation for the #2396 path: when a GitNexus process holds the
396
+ * lbug DB write lock the CLI `augment` can't run, so point the agent at the MCP
397
+ * `query` tool instead. Phrased conditionally ("if the MCP tools are live") so it
398
+ * stays truthful on every owner path — a confirmed MCP owner, a `serve` owner, or
399
+ * a fail-closed probe where no server is actually confirmed. `pattern` is embedded
400
+ * verbatim; the caller (writeAdditionalContext) JSON-escapes it structurally.
401
+ */
402
+ function buildMcpQueryHint(pattern) {
403
+ return (
404
+ `[GitNexus] Local augment is unavailable (the graph DB is held by another ` +
405
+ `GitNexus process). If the GitNexus MCP tools are live in this session, call ` +
406
+ `the GitNexus \`query\` MCP tool (e.g. mcp__gitnexus__query) with ` +
407
+ `search_query "${pattern}".`
408
+ );
409
+ }
410
+
411
+ /**
412
+ * #2396 throttle: emit the MCP-query hint at most once per repo per window, so an
413
+ * owner-locked session isn't nudged on every search. Window (ms) via
414
+ * GITNEXUS_MCP_HINT_THROTTLE_MS (default 10min; 0/invalid disables). Best-effort —
415
+ * any fs error falls back to emitting.
416
+ * ponytail: per-repo mtime marker, shared across concurrent sessions on the same
417
+ * repo; add per-session dedup only if that sharing becomes a problem.
418
+ */
419
+ function shouldEmitMcpHint(gitNexusDir) {
420
+ const raw = process.env.GITNEXUS_MCP_HINT_THROTTLE_MS;
421
+ const windowMs = raw === undefined || raw === '' ? 600000 : Number(raw);
422
+ if (!Number.isFinite(windowMs) || windowMs <= 0) return true;
423
+ const marker = path.join(gitNexusDir, '.mcp-hint-shown');
424
+ try {
425
+ if (Date.now() - fs.statSync(marker).mtimeMs < windowMs) return false;
426
+ } catch {
427
+ /* marker missing/unreadable → emit */
428
+ }
429
+ try {
430
+ fs.writeFileSync(marker, '');
431
+ } catch {
432
+ /* best-effort; still emit */
433
+ }
434
+ return true;
435
+ }
436
+
394
437
  function runAugment(gitNexusDir, cwd, pattern) {
395
438
  // Acquire the per-repo slot BEFORE the DB-owner probe (#2163): the probe
396
439
  // itself spawns lsof/ps, so it must be bounded by the same ≤3-per-repo cap
@@ -410,12 +453,16 @@ function runAugment(gitNexusDir, cwd, pattern) {
410
453
  }
411
454
  try {
412
455
  if (hasGitNexusServerOwner(gitNexusDir)) {
413
- // Normal skip path: the MCP server owns the DB. Stay silent for strict
414
- // hook runners (issue #1913); surface the reason only under GITNEXUS_DEBUG.
456
+ // #2396: the MCP server holds the DB write lock, so a competing CLI
457
+ // `augment` would only contend on it (LadybugDB is single-writer). The
458
+ // session has the GitNexus MCP tools live — route the augmentation to the
459
+ // agent via additionalContext instead of dropping it. Mirror the skip
460
+ // reason to stderr only under GITNEXUS_DEBUG (strict-runner contract,
461
+ // #1913); the hint itself rides the sanctioned additionalContext channel.
415
462
  if (isDebugEnabled()) {
416
463
  process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
417
464
  }
418
- return '';
465
+ return shouldEmitMcpHint(gitNexusDir) ? buildMcpQueryHint(pattern) : '';
419
466
  }
420
467
  const cliPath = resolveCliPath();
421
468
  const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);